# -*- coding: utf-8 -*-
# all in one version
"""
Copyright (c) 2018 Keijack Wu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

name = "tweb" # tiny web engine
version = "1.1.4"
import time
# 
start_time = time.time()
# file: utils/logger.py
from abc import abstractmethod
import sys
import time
import logging
import asyncio
from threading import Thread
from typing import Dict, List, Tuple


class LazyCalledLogger(logging.Logger):

    def do_call_handlers(self, record):
        super().callHandlers(record)

    @abstractmethod
    def callHandlers(self, record):
        pass


class LazyCalledLoggerThread:

    daemon_threads = True

    def __init__(self) -> None:
        self.coroutine_loop = None
        self.coroutine_thread = None

    def coroutine_main(self):
        self.coroutine_loop = loop = asyncio.new_event_loop()
        try:
            loop.run_forever()
        finally:
            loop.run_until_complete(loop.shutdown_asyncgens())
            loop.close()

    def start(self):
        if self.coroutine_thread is not None:
            while not self.coroutine_loop:
                # wait for the loop ready
                time.sleep(0.1)
            return
        self.coroutine_thread = Thread(target=self.coroutine_main, name="logger-thread", daemon=self.daemon_threads)
        self.coroutine_thread.start()

        while not self.coroutine_loop:
            # wait for the loop ready
            time.sleep(0.1)

    def stop(self):
        if self.coroutine_loop:
            self.coroutine_loop.call_soon_threadsafe(self.coroutine_loop.stop)
            self.coroutine_thread.join()
            self.coroutine_loop = None
            self.coroutine_thread = None

    async def _call(self, logger: LazyCalledLogger, record):
        logger.do_call_handlers(record)

    def call_logger_handler(self, logger: LazyCalledLogger, record):
        self.start()
        asyncio.run_coroutine_threadsafe(self._call(logger, record), self.coroutine_loop)


class CachingLogger(LazyCalledLogger):

    _custom_logger: logging.Logger = None

    logger_thread: LazyCalledLoggerThread = LazyCalledLoggerThread()

    def callHandlers(self, record):
        if CachingLogger._custom_logger:
            CachingLogger._custom_logger.callHandlers(record)
            return
        CachingLogger.logger_thread.call_logger_handler(self, record)


def set_custom_logger(logger: logging.Logger) -> None:
    CachingLogger._custom_logger = logger


class LoggerFactory:

    # DEFAULT_LOG_FORMAT: str = '[%(asctime)s]-[%(threadName)s]-[%(name)s:%(lineno)d] %(levelname)-4s: %(message)s'
    DEFAULT_LOG_FORMAT: str = '[%(asctime)s]-[%(filename)s:%(lineno)d] %(levelname)-4s: %(message)s'

    DEFAULT_DATE_FORMAT: str = '%Y-%m-%d %H:%M:%S'

    _LOG_LVELS: Tuple[str] = ("DEBUG", "INFO", "WARN", "ERROR")

    def __init__(self, log_level: str = "INFO", log_format: str = DEFAULT_LOG_FORMAT, date_format: str = DEFAULT_DATE_FORMAT) -> None:
        self.__cache_loggers: Dict[str, CachingLogger] = {}
        self._log_level = log_level.upper() if log_level and log_level.upper() in self._LOG_LVELS else "INFO"
        self.log_format = log_format
        self.date_format = date_format

        self._handlers: List[logging.Handler] = []

    @property
    def handlers(self) -> List[logging.Handler]:
        if self._handlers:
            return self._handlers
        _handler = logging.StreamHandler(sys.stdout)
        _formatter_ = logging.Formatter(fmt=self.log_format, datefmt=self.date_format)
        _handler.setFormatter(_formatter_)
        _handler.setLevel(self._log_level)
        self._handlers.append(_handler)
        return self._handlers

    @property
    def log_level(self):
        return self._log_level

    @log_level.setter
    def log_level(self, log_level: str):
        self._log_level = log_level.upper() if log_level and log_level.upper() in self._LOG_LVELS else "INFO"
        _logger_ = self.get_logger()
        _logger_.info(f"global logger set to {self._log_level}")
        for h in self._handlers:
            h.setLevel(self._log_level)
        for l in self.__cache_loggers.values():
            l.setLevel(self._log_level)

    def add_handler(self, handler: logging.Handler) -> None:
        if self.__cache_loggers:
            self._handlers.append(handler)
        else:
            self.handlers.append(handler)
        for l in self.__cache_loggers.values():
            l.addHandler(handler)

    def remove_handler(self, handler: logging.Handler) -> None:
        if handler in self._handlers:
            self._handlers.remove(handler)
        for l in self.__cache_loggers.values():
            l.removeHandler(handler)

    def set_handler(self, handler: logging.Handler) -> None:
        self._handlers.clear()
        self._handlers.append(handler)
        for l in self.__cache_loggers.values():
            for hdlr in l.handlers:
                l.removeHandler(hdlr)
            l.addHandler(handler)

    def get_logger(self, tag: str = "") -> logging.Logger:
        if tag not in self.__cache_loggers:
            self.__cache_loggers[tag] = CachingLogger(tag, self._log_level)
            for hdlr in self.handlers:
                self.__cache_loggers[tag].addHandler(hdlr)
        return self.__cache_loggers[tag]


_default_logger_factory: LoggerFactory = LoggerFactory()
_logger_factories: Dict[str, LoggerFactory] = {}


def get_logger_factory(tag: str = "") -> LoggerFactory:
    if not tag:
        return _default_logger_factory
    if tag not in _logger_factories:
        _logger_factories[tag] = LoggerFactory()
    return _logger_factories[tag]


def set_level(level) -> None:
    _default_logger_factory.log_level = level


def add_handler(handler: logging.Handler) -> None:
    _default_logger_factory.add_handler(handler)


def remove_handler(handler: logging.Handler) -> None:
    _default_logger_factory.remove_handler(handler)


def set_handler(handler: logging.Handler) -> None:
    _default_logger_factory.set_handler(handler)


def get_logger(tag: str = "", factory: str = "") -> logging.Logger:
    return get_logger_factory(factory).get_logger(tag)

_logger = get_logger("tweb")
# file: models.py
# -*- coding: utf-8 -*-
import http.cookies
import time
from abc import abstractmethod
from typing import Any, Dict, List, Tuple, Union


DEFAULT_ENCODING: str = "UTF-8"

SESSION_COOKIE_NAME: str = "PY_SIM_HTTP_SER_SESSION_ID"

WEBSOCKET_OPCODE_CONTINUATION: int = 0x0
WEBSOCKET_OPCODE_TEXT: int = 0x1
WEBSOCKET_OPCODE_BINARY: int = 0x2
WEBSOCKET_OPCODE_CLOSE: int = 0x8
WEBSOCKET_OPCODE_PING: int = 0x9
WEBSOCKET_OPCODE_PONG: int = 0xA

WEBSOCKET_MESSAGE_TEXT: str = "WEBSOCKET_MESSAGE_TEXT"
WEBSOCKET_MESSAGE_BINARY: str = "WEBSOCKET_MESSAGE_BINARY"
WEBSOCKET_MESSAGE_BINARY_FRAME: str = "WEBSOCKET_MESSAGE_BINARY_FRAME"
WEBSOCKET_MESSAGE_PING: str = "WEBSOCKET_MESSAGE_PING"
WEBSOCKET_MESSAGE_PONG: str = "WEBSOCKET_MESSAGE_PONG"


class HttpSession:

    def __init__(self):
        self.max_inactive_interval: int = 30 * 60

    @property
    def id(self) -> str:
        return ""

    @property
    def creation_time(self) -> float:
        return 0

    @property
    def last_accessed_time(self) -> float:
        return 0

    @property
    def attribute_names(self) -> Tuple:
        return ()

    @property
    def is_new(self) -> bool:
        return False

    @property
    def is_valid(self) -> bool:
        return time.time() - self.last_accessed_time < self.max_inactive_interval

    @abstractmethod
    def get_attribute(self, name: str) -> Any:
        return NotImplemented

    @abstractmethod
    def set_attribute(self, name: str, value: str) -> None:
        return NotImplemented

    @abstractmethod
    def invalidate(self) -> None:
        return NotImplemented


class HttpSessionFactory:

    @abstractmethod
    def get_session(self, session_id: str, create: bool = False) -> HttpSession:
        return NotImplemented


class Cookies(http.cookies.SimpleCookie):
    EXPIRE_DATE_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"


class RequestBodyReader:

    @abstractmethod
    async def read(self, n: int = -1) -> bytes:
        return NotImplemented

    @abstractmethod
    async def read_to_end(self) -> bytes:
        return NotImplemented


class Request:
    """Request"""

    def __init__(self):
        self.method: str = ""  # GET, POST, PUT, DELETE, HEAD, etc.
        self.headers: Dict[str, str] = {}  # Request headers
        self.__cookies = Cookies()
        self.query_string: str = ""  # Query String
        self.path_values: Dict[str, str] = {}
        self.reg_groups = ()  # If controller is matched via regexp, then ,all groups are save here
        self.path: str = ""  # Path
        self.__parameters = {}  # Parameters, key-value array, merged by query string and request body if the `Content-Type` in request header is `application/x-www-form-urlencoded` or `multipart/form-data`
        # Parameters, key-value, if more than one parameters with the same key, only the first one will be stored.
        self.__parameter = {}
        self._body: bytes = b""  # Request body
        # A dictionary if the `Content-Type` in request header is `application/json`
        self.json: Dict[str, Any] = None
        self.environment = {}
        self.reader: RequestBodyReader = None  # A stream reader

    @property
    def cookies(self) -> Cookies:
        return self.__cookies

    @property
    def parameters(self) -> Dict[str, List[str]]:
        return self.__parameters

    @parameters.setter
    def parameters(self, val: Dict[str, List[str]]):
        self.__parameters = val
        self.__parameter = {}
        for k, v in self.__parameters.items():
            self.__parameter[k] = v[0]

    @property
    def parameter(self) -> Dict[str, str]:
        return self.__parameter

    @property
    def host(self) -> str:
        if "Host" not in self.headers:
            return ""
        else:
            return self.headers["Host"]

    @property
    def body(self) -> bytes:
        return self._body

    @property
    def content_type(self) -> str:
        if "Content-Type" not in self.headers:
            return ""
        else:
            return self.headers["Content-Type"]

    @property
    def content_length(self) -> int:
        if "Content-Length" not in self.headers:
            return None
        else:
            return int(self.headers["Content-Length"])

    def get_parameter(self, key: str, default: str = None) -> str:
        if key not in self.parameters.keys():
            return default
        else:
            return self.parameter[key]

    @abstractmethod
    def get_session(self, create: bool = False) -> HttpSession:
        return NotImplemented


class MultipartFile:
    """Multipart file"""

    def __init__(self, name: str = "",
                 required: bool = False,
                 filename: str = "",
                 content_type: str = "",
                 content: bytes = None):
        self.__name = name
        self.__required = required
        self.__filename = filename
        self.__content_type = content_type
        self.__content = content

    @property
    def name(self) -> str:
        return self.__name

    @property
    def _required(self) -> bool:
        return self.__required

    @property
    def filename(self) -> str:
        return self.__filename

    @property
    def content_type(self) -> str:
        return self.__content_type

    @property
    def content(self) -> bytes:
        return self.__content

    @property
    def is_empty(self) -> bool:
        return self.__content is None or len(self.__content) == 0

    def save_to_file(self, file_path: str) -> None:
        if self.__content is not None and len(self.__content) > 0:
            with open(file_path, "wb") as f:
                f.write(self.__content)


class ParamStringValue(str):

    def __init__(self, name: str = "",
                 default: str = "",
                 required: bool = False):
        self.__name = name
        self.__required = required

    @property
    def name(self) -> str:
        return self.__name

    @property
    def _required(self) -> bool:
        return self.__required

    def __new__(cls, name="", default="", **kwargs):
        assert isinstance(default, str)
        obj = super().__new__(cls, default)
        return obj


class Parameter(ParamStringValue):
    pass


class PathValue(str):

    def __init__(self, name: str = "", _value: str = ""):
        self.__name = name

    @property
    def name(self):
        return self.__name

    def __new__(cls, name: str = "", _value: str = "", **kwargs):
        assert isinstance(_value, str)
        obj = super().__new__(cls, _value)
        return obj


class Parameters(list):

    def __init__(self, name: str = "", default: List[str] = [], required: bool = False):
        self.__name = name
        self.__required = required

    @property
    def name(self) -> str:
        return self.__name

    @property
    def _required(self) -> bool:
        return self.__required

    def __new__(cls, name: str = "", default: List[str] = [], **kwargs):
        obj = super().__new__(cls)
        obj.extend(default)
        return obj


class ModelDict(dict):
    pass


class Environment(dict):
    pass


class RegGroups(tuple):
    pass


class RegGroup(str):

    def __init__(self, group=0, **kwargs):
        self._group = group

    @property
    def group(self) -> int:
        return self._group

    def __new__(cls, group=0, **kwargs):
        if "_value" not in kwargs:
            val = ""
        else:
            val = kwargs["_value"]
        obj = super().__new__(cls, val)
        return obj


class Header(ParamStringValue):
    pass


class JSONBody(dict):
    pass


class BytesBody(bytes):
    pass


"""
" The folowing beans are used in Response
"""


class StaticFile:

    def __init__(self, file_path, content_type="application/octet-stream"):
        self.file_path = file_path
        self.content_type = content_type


class Response:
    """Response"""

    def __init__(self,
                 status_code: int = 200,
                 headers: Dict[str, str] = None,
                 body: Union[str, dict, StaticFile, bytes] = ""):
        self.status_code = status_code
        self.__headers = headers if headers is not None else {}
        self.__body = ""
        self.__cookies = Cookies()
        self.__set_body(body)

    @property
    def cookies(self) -> http.cookies.SimpleCookie:
        return self.__cookies

    @cookies.setter
    def cookies(self, val: http.cookies.SimpleCookie) -> None:
        assert isinstance(val, http.cookies.SimpleCookie)
        self.__cookies = val

    @property
    def body(self):
        return self.__body

    @body.setter
    def body(self, val: Union[str, dict, StaticFile, bytes]) -> None:
        self.__set_body(val)

    def __set_body(self, val):
        assert val is None \
            or isinstance(val, str) \
            or isinstance(val, dict) \
            or isinstance(val, StaticFile) \
            or isinstance(val, bytes) \
            or isinstance(val, bytearray), \
            "Body type is not supported."
        self.__body = val

    @property
    def headers(self) -> Dict[str, Union[list, str]]:
        return self.__headers

    def set_header(self, key: str, value: str) -> None:
        self.__headers[key] = value

    def add_header(self, key: str, value: Union[str, list]) -> None:
        if key not in self.__headers.keys():
            self.__headers[key] = value
            return
        if not isinstance(self.__headers[key], list):
            self.__headers[key] = [self.__headers[key]]
        if isinstance(value, list):
            self.__headers[key].extend(value)
        else:
            self.__headers[key].append(value)

    def add_headers(self, headers: Dict[str, Union[str, List[str]]] = {}) -> None:
        if headers is not None:
            for k, v in headers.items():
                self.add_header(k, v)

    @abstractmethod
    def send_error(self, status_code: int, message: str = ""):
        return NotImplemented

    @abstractmethod
    def send_redirect(self, url: str):
        return NotImplemented

    @abstractmethod
    def send_response(self):
        return NotImplemented

    @abstractmethod
    def write_bytes(self, data: bytes):
        pass

    @abstractmethod
    def close(self):
        pass


class HttpError(Exception):

    def __init__(self, code: int = 400, message: str = "", explain: str = ""):
        super().__init__("HTTP_ERROR[%d] %s" % (code, message))
        self.code: int = code
        self.message: str = message
        self.explain: str = explain


class Redirect:

    def __init__(self, url: str):
        self.__url = url

    @property
    def url(self) -> str:
        return self.__url


"""
" Use both in request and response
"""


class Headers(dict):

    def __init__(self, headers: Dict[str, Union[str, List[str]]] = {}):
        self.update(headers)


class Cookie(http.cookies.Morsel):

    def __init__(self,
                 name: str = "",
                 default: str = "",
                 default_options: Dict[str, str] = {},
                 required: bool = False):
        super().__init__()
        self.__name = name
        self.__required = required
        if name is not None and name != "":
            self.set(name, default, default)
        self.update(default_options)

    @property
    def name(self) -> str:
        return self.__name

    @property
    def _required(self) -> bool:
        return self.__required


class FilterContext:

    @property
    def request(self) -> Request:
        return NotImplemented

    @property
    def response(self) -> Response:
        return NotImplemented

    @abstractmethod
    def do_chain(self):
        return NotImplemented


class WebsocketRequest:

    def __init__(self):
        self.headers: Dict[str, str] = {}  # Request headers
        self.__cookies = Cookies()
        self.query_string: str = ""  # Query String
        self.path_values: Dict[str, str] = {}
        self.reg_groups = ()  # If controller is matched via regexp, then ,all groups are save here
        self.path: str = ""  # Path
        self.__parameters = {}  # Parameters, key-value array, merged by query string and request body if the `Content-Type` in request header is `application/x-www-form-urlencoded` or `multipart/form-data`
        # Parameters, key-value, if more than one parameters with the same key, only the first one will be stored.
        self.__parameter = {}

    @property
    def cookies(self) -> Cookies:
        return self.__cookies

    @property
    def parameters(self) -> Dict[str, List[str]]:
        return self.__parameters

    @parameters.setter
    def parameters(self, val: Dict[str, List[str]]):
        self.__parameters = val
        self.__parameter = {}
        for k, v in self.__parameters.items():
            self.__parameter[k] = v[0]

    @property
    def parameter(self) -> Dict[str, str]:
        return self.__parameter

    def get_parameter(self, key: str, default: str = None) -> str:
        if key not in self.parameters.keys():
            return default
        else:
            return self.parameter[key]


class WebsocketSession:

    @property
    def id(self) -> str:
        return NotImplemented

    @property
    def request(self) -> WebsocketRequest:
        return NotImplemented

    @property
    def is_closed(self) -> bool:
        return NotImplemented

    @abstractmethod
    def send(self, message: Union[str, bytes], opcode: int = None, chunk_size: int = 0):
        return NotImplemented

    @abstractmethod
    def send_text(self, message: str, chunk_size: int = 0):
        return NotImplemented

    @abstractmethod
    def send_binary(self, binary: bytes, chunk_size: int = 0):
        return NotImplemented

    @abstractmethod
    def send_file(self, path: str, chunk_size: int = 0):
        return NotImplemented

    @abstractmethod
    def send_pone(self, message: bytes = b''):
        return NotImplemented

    @abstractmethod
    def send_ping(self, message: bytes = b''):
        return NotImplemented

    @abstractmethod
    def close(self, reason: str = ""):
        return NotImplemented


class WebsocketCloseReason(str):

    def __init__(self,
                 message: str = "",
                 code: int = None,
                 reason: str = '') -> None:
        self.__message: str = message
        self.__code: int = code
        self.__reason: str = reason

    @property
    def message(self) -> str:
        return self.__message

    @property
    def code(self) -> int:
        return self.__code

    @property
    def reason(self) -> str:
        return self.__reason

    def __new__(cls, message: str = "", code: int = "", reason: str = '', **kwargs):
        obj = super().__new__(cls, message)
        return obj


class WebsocketHandler:

    @abstractmethod
    def on_handshake(self, request: WebsocketRequest = None):
        """
        "
        " You can get path/headers/path_values/cookies/query_string/query_parameters from request.
        "
        " You should return a tuple means (http_status_code, headers)
        "
        " If status code in (0, None, 101), the websocket will be connected, or will return the status you return.
        "
        " All headers will be send to client
        "
        """
        return None

    @abstractmethod
    def on_open(self, session: WebsocketSession = None):
        """
        "
        " Will be called when the connection opened.
        "
        """
        pass

    @abstractmethod
    def on_close(self, session: WebsocketSession = None, reason: WebsocketCloseReason = None):
        """
        "
        " Will be called when the connection closed.
        "
        """
        pass

    @abstractmethod
    def on_ping_message(self, session: WebsocketSession = None, message: bytes = b''):
        """
        "
        " Will be called when receive a ping message. Will send all the message bytes back to client by default.
        "
        """
        session.send_pone(message)

    @abstractmethod
    def on_pong_message(self, session: WebsocketSession = None, message: bytes = ""):
        """
        "
        " Will be called when receive a pong message.
        "
        """
        pass

    @abstractmethod
    def on_text_message(self, session: WebsocketSession = None, message: str = ""):
        """
        "
        " Will be called when receive a text message.
        "
        """
        pass

    @abstractmethod
    def on_binary_message(self, session: WebsocketSession = None, message: bytes = b''):
        """
        "
        " Will be called when receive a binary message if you have not consumed all the bytes in `on_binary_frame`
        " method.
        "
        """
        pass

    @abstractmethod
    def on_binary_frame(self, session: WebsocketSession = None, fin: bool = False, frame_payload: bytes = b''):
        """
        "
        " When server receive a fragmented message, this method will be called every time when a frame is received,
        " you can consume all the bytes in this method, e.g. save all bytes to a file.
        "
        " If you does not implement this method or return a True in this method, all the bytes will be cached in
        " memory and sent to your `on_binary_message` method after all frames are received.
        "
        """
        return True

# file: request_handlers/model_bindings.py
# -*- coding: utf-8 -*-
import json
import sys
from typing import Any, Dict, List, Type
from http.cookies import BaseCookie, SimpleCookie
# from ..models import ModelDict, Environment, RegGroup, RegGroups, HttpError, RequestBodyReader, \
#     Headers, Response, Cookies, Cookie, JSONBody, BytesBody, Header, Parameters, PathValue, Parameter, \
#     MultipartFile, Request, HttpSession


class ModelBinding:

    def __init__(self, request: Request,
                 response: Response,
                 arg: str,
                 arg_type,
                 val=None
                 ) -> None:
        self.request = request
        self.response = response
        self.arg_name = arg
        self.arg_type = arg_type
        self.default_value = val
        self._kws = {}
        if self.default_value is not None:
            self._kws["val"] = self.default_value

    async def bind(self) -> Any:
        pass


class RequestModelBinding(ModelBinding):

    async def bind(self) -> Request:
        return self.request


class SessionModelBinding(ModelBinding):

    async def bind(self) -> HttpSession:
        return self.request.get_session(True)


class ResponseModelBinding(ModelBinding):

    async def bind(self) -> Response:
        return self.response


class HeadersModelBinding(ModelBinding):

    async def bind(self) -> Headers:
        return Headers(self.request.headers)


class RegGroupsModelBinding(ModelBinding):

    async def bind(self) -> RegGroups:
        return RegGroups(self.request.reg_groups)


class EnvironmentModelBinding(ModelBinding):

    async def bind(self) -> Environment:
        return Environment(self.request.environment)


class HeaderModelBinding(ModelBinding):

    async def bind(self) -> Header:
        return self.__build_header(self.arg_name, **self._kws)

    def __build_header(self, key, val=Header()):
        name = val.name if val.name is not None and val.name != "" else key
        if val._required and name not in self.request.headers:
            raise HttpError(400, "Missing Header",
                            f"Header[{name}] is required.")
        if name in self.request.headers:
            v = self.request.headers[name]
            return Header(name=name, default=v, required=val._required)
        else:
            return val


class CookiesModelBinding(ModelBinding):

    async def bind(self) -> Cookies:
        return self.request.cookies


class CookieModelBinding(ModelBinding):

    async def bind(self) -> Cookie:
        return self.__build_cookie(self.arg_name, **self._kws)

    def __build_cookie(self, key, val=None):
        name = val.name if val.name is not None and val.name != "" else key
        if val._required and name not in self.request.cookies:
            raise HttpError(400, "Missing Cookie",
                            f"Cookie[{name}] is required.")
        if name in self.request.cookies:
            morsel = self.request.cookies[name]
            cookie = Cookie()
            cookie.set(morsel.key, morsel.value, morsel.coded_value)
            cookie.update(morsel)
            return cookie
        else:
            return val


class MultipartFileModelBinding(ModelBinding):

    async def bind(self) -> MultipartFile:
        return self.__build_multipart(self.arg_name, **self._kws)

    def __build_multipart(self, key, val=MultipartFile()):
        name = val.name if val.name is not None and val.name != "" else key
        if val._required and name not in self.request.parameter.keys():
            raise HttpError(400, "Missing Parameter",
                            f"Parameter[{name}] is required.")
        if name in self.request.parameter.keys():
            v = self.request.parameter[name]
            if isinstance(v, MultipartFile):
                return v
            else:
                raise HttpError(
                    400, None, f"Parameter[{name}] should be a file.")
        else:
            return val


class ParameterModelBinding(ModelBinding):

    async def bind(self) -> Parameter:
        return self.__build_param(self.arg_name, **self._kws)

    def __build_param(self, key, val=Parameter()):
        name = val.name if val.name is not None and val.name != "" else key
        if val._required and name not in self.request.parameter:
            raise HttpError(400, "Missing Parameter",
                            f"Parameter[{name}] is required.")
        if name in self.request.parameter:
            v = self.request.parameter[name]
            return Parameter(name=name, default=v, required=val._required)
        else:
            return val


class PathValueModelBinding(ModelBinding):

    async def bind(self) -> PathValue:
        return self.__build_path_value(self.arg_name, **self._kws)

    def __build_path_value(self, key, val=PathValue()):
        # wildcard value
        if len(self.request.path_values) == 1 and "__path_wildcard" in self.request.path_values:
            if val.name:
                _logger.warning(
                    f"Wildcard value, `name` of the PathValue:: [{val.name}] will be ignored. ")
            return self.request.path_values["__path_wildcard"]

        # brace values
        name = val.name if val.name is not None and val.name != "" else key
        if name in self.request.path_values:
            return PathValue(name=name, _value=self.request.path_values[name])
        else:
            raise HttpError(
                500, None, f"path name[{name}] not in your url mapping!")


class ParametersModelBinding(ModelBinding):

    async def bind(self) -> Parameters:
        return self.__build_params(self.arg_name, **self._kws)

    def __build_params(self, key, val=Parameters()):
        name = val.name if val.name is not None and val.name != "" else key
        if val._required and name not in self.request.parameters:
            raise HttpError(400, "Missing Parameter",
                            f"Parameter[{name}] is required.")
        if name in self.request.parameters:
            v = self.request.parameters[name]
            return Parameters(name=name, default=v, required=val._required)
        else:
            return val


class RegGroupModelBinding(ModelBinding):

    async def bind(self) -> RegGroup:
        return self.__build_reg_group(**self._kws)

    def __build_reg_group(self, val: RegGroup = RegGroup(group=0)):
        if val.group >= len(self.request.reg_groups):
            raise HttpError(400, None, f"RegGroup required an element at {val.group}, but the reg length is only {len(self.request.reg_groups)}")
        return RegGroup(group=val.group, _value=self.request.reg_groups[val.group])


class JSONBodyModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_json_body()

    def __build_json_body(self):
        if "content-type" not in self.request._headers_keys_in_lowcase.keys() or \
                not self.request._headers_keys_in_lowcase["content-type"].lower().startswith("application/json"):
            raise HttpError(400, None, 'The content type of this request must be "application/json"')
        return JSONBody(self.request.json)


class RequestBodyReaderModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.request.reader


class BytesBodyModelBinding(ModelBinding):

    async def bind(self) -> Any:
        if not self.request._body:
            self.request._body = await self.request.reader.read_to_end()
        return BytesBody(self.request._body)


class StrModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_str(self.arg_name, **self._kws)

    def __build_str(self, key, val=None):
        if key in self.request.parameter.keys():
            return Parameter(name=key, default=self.request.parameter[key], required=False)
        elif val is None:
            return None
        else:
            return Parameter(name=key, default=val, required=False)


class BoolModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_bool(self.arg_name, **self._kws)

    def __build_bool(self, key, val=None):
        if key in self.request.parameter.keys():
            v = self.request.parameter[key]
            return v.lower() not in ("0", "false", "")
        else:
            return val


class IntModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_int(self.arg_name, **self._kws)

    def __build_int(self, key, val=None):
        if key in self.request.parameter.keys():
            try:
                return int(self.request.parameter[key])
            except:
                raise HttpError(
                    400, None, f"Parameter[{key}] should be an int. ")
        else:
            return val


class FloatModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_float(self.arg_name, **self._kws)

    def __build_float(self, key, val=None):
        if key in self.request.parameter.keys():
            try:
                return float(self.request.parameter[key])
            except:
                raise HttpError(
                    400, None, f"Parameter[{key}] should be an float. ")
        else:
            return val


class ListModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_list(self.arg_name, **self._kws)

    def __build_list(self, key, target_type=list, val=[]):
        if key in self.request.parameters.keys():
            ori_list = self.request.parameters[key]
        else:
            ori_list = val

        if target_type == List[int]:
            try:
                return [int(p) for p in ori_list]
            except:
                raise HttpError(
                    400, None, f"One of the parameter[{key}] is not int. ")
        elif target_type == List[float]:
            try:
                return [float(p) for p in ori_list]
            except:
                raise HttpError(
                    400, None, f"One of the parameter[{key}] is not float. ")
        elif target_type == List[bool]:
            return [p.lower() not in ("0", "false", "") for p in ori_list]
        elif target_type in (List[dict], List[Dict]):
            try:
                return [json.loads(p) for p in ori_list]
            except:
                raise HttpError(
                    400, None, f"One of the parameter[{key}] is not JSON string. ")
        elif target_type == List[Parameter]:
            return [Parameter(name=key, default=p, required=False) for p in ori_list]
        else:
            return ori_list


class ModelDictModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_model_dict()

    def __build_model_dict(self):
        mdict = ModelDict()
        for k, v in self.request.parameters.items():
            if len(v) == 1:
                mdict[k] = v[0]
            else:
                mdict[k] = v
        return mdict


class DictModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.__build_dict(self.arg_name, **self._kws)

    def __build_dict(self, key, val={}):
        if key in self.request.parameter.keys():
            try:
                return json.loads(self.request.parameter[key])
            except:
                raise HttpError(
                    400, None, f"Parameter[{key}] should be a JSON string.")
        else:
            return val


class DefaultModelBinding(ModelBinding):

    async def bind(self) -> Any:
        return self.default_value


class ModelBindingConf:

    def __init__(self) -> None:
        self.default_model_binding_type = DefaultModelBinding
        self.model_bingding_types: Dict[Type, Type[ModelBinding]] = {
            Request: RequestModelBinding,
            HttpSession: SessionModelBinding,
            Response: ResponseModelBinding,
            Headers: HeadersModelBinding,
            RegGroups: RegGroupsModelBinding,
            Environment: EnvironmentModelBinding,
            Header: HeaderModelBinding,
            Cookies: CookiesModelBinding,
            BaseCookie: CookiesModelBinding,
            SimpleCookie: CookiesModelBinding,
            Cookie: CookieModelBinding,
            MultipartFile: MultipartFileModelBinding,
            Parameter: ParameterModelBinding,
            PathValue: PathValueModelBinding,
            Parameters: ParametersModelBinding,
            RegGroup: RegGroupModelBinding,
            JSONBody: JSONBodyModelBinding,
            RequestBodyReader: RequestBodyReaderModelBinding,
            BytesBody: BytesBodyModelBinding,
            str: StrModelBinding,
            bool: BoolModelBinding,
            int: IntModelBinding,
            float: FloatModelBinding,
            list: ListModelBinding,
            List: ListModelBinding,
            List[str]: ListModelBinding,
            List[Parameter]: ListModelBinding,
            List[int]: ListModelBinding, 
            List[float]: ListModelBinding,
            List[bool]: ListModelBinding,
            List[dict]: ListModelBinding,
            List[Dict]: ListModelBinding,
            ModelDict: ModelDictModelBinding,
            dict: DictModelBinding,
            Dict: DictModelBinding
        }
        if sys.version_info >= (3, 9):
            self.model_bingding_types[list[str]] = ListModelBinding
            self.model_bingding_types[list[Parameter]] = ListModelBinding
            self.model_bingding_types[list[int]] = ListModelBinding
            self.model_bingding_types[list[bool]] = ListModelBinding
            self.model_bingding_types[list[float]] = ListModelBinding
            self.model_bingding_types[list[Dict]] = ListModelBinding
            self.model_bingding_types[list[dict]] = ListModelBinding

# file: app_conf.py
# -*- coding: utf-8 -*-
import sys
import inspect
import asyncio
import base64
from typing import Any, Dict, List, Type, Union, Callable

# from .request_handlers.http_session_local_impl import LocalSessionFactory
# from .models import Headers, HttpSessionFactory, WebsocketHandler
# from .models import WEBSOCKET_MESSAGE_BINARY, WEBSOCKET_MESSAGE_BINARY_FRAME, WEBSOCKET_MESSAGE_PING, WEBSOCKET_MESSAGE_PONG, WEBSOCKET_MESSAGE_TEXT
# from .request_handlers.model_bindings import ModelBindingConf


def _get_class_of_method(method_defind):
    vals = vars(sys.modules[method_defind.__module__])
    for attr in method_defind.__qualname__.split('.')[:-1]:
        if attr in vals:
            vals = vals[attr]
            if not isinstance(vals, dict):
                break
    if inspect.isclass(vals):
        return vals


def _create_object(clz, args=[], kwargs={}):

    if clz is None:
        return None
    elif args and kwargs:
        return clz(*args, **kwargs)
    elif args:
        return clz(*args)
    elif kwargs:
        return clz(**kwargs)
    else:
        return clz()


def _to_str_list(obj: Union[str, list, tuple]):
    if isinstance(obj, list) or isinstance(obj, tuple):
        return [str(it) for it in obj]
    else:
        return [str(obj)]


class _ControllerFunction:

    def __init__(self, url: str = "",
                 regexp: str = "",
                 method: str = "",
                 headers: List[str] = [],
                 match_all_headers_expressions: bool = None,
                 params: List[str] = [],
                 match_all_params_expressions: bool = None,
                 func: Callable = None) -> None:
        self.__url: str = url
        self.__regexp = regexp
        self.__method: str = method
        self.singleton: bool = False
        self.ctr_obj_init_args = None
        self.ctr_obj_init_kwargs = None
        self.__ctr_obj: object = None
        self.__func: Callable = func
        self.__clz = False
        self.headers: List[str] = headers if isinstance(headers, list) else [
            headers]
        self._match_all_headers_expressions: bool = match_all_headers_expressions
        self.params: List[str] = params if isinstance(params, list) else [
            params]
        self._match_all_params_expressions: bool = match_all_params_expressions

    @property
    def match_all_headers_expressions(self):
        return self._match_all_headers_expressions if self._match_all_headers_expressions is not None else True

    @property
    def match_all_params_expressions(self):
        return self._match_all_params_expressions if self._match_all_params_expressions is not None else True

    @property
    def _is_config_ok(self):
        try:
            assert self.url or self.regexp, "you should set one of url and regexp"
            assert not (
                self.url and self.regexp), "you can only set one of url and regexp, not both"
            assert self.func is not None and (inspect.isfunction(
                self.func) or inspect.ismethod(self.func))
            return True
        except AssertionError as ae:
            _logger.warn(
                f"[{self.__url}|{self.regexp}] => {self.func} configurate error: {ae}")
            return False

    @property
    def url(self) -> str:
        return self.__url

    @property
    def regexp(self) -> str:
        return self.__regexp

    @property
    def method(self) -> str:
        return self.__method

    @property
    def clz(self) -> str:
        if self.__clz != False:
            return self.__clz
        if inspect.isfunction(self.func):
            self.__clz = _get_class_of_method(self.func)
        else:
            self.__clz = None
        return self.__clz

    @property
    def ctrl_object(self) -> object:
        if not self.singleton:
            obj = self._create_ctrl_obj()
            _logger.debug(f"singleton: create a object -> {obj}")
            return obj

        if self.__ctr_obj is None:
            self.__ctr_obj = self._create_ctrl_obj()
            _logger.debug(
                f"object does not exist, create one -> {self.__ctr_obj} ")
        else:
            _logger.debug(f"object[{self.__ctr_obj}] exists, return. ")
        return self.__ctr_obj

    def _create_ctrl_obj(self) -> object:
        return _create_object(self.clz, self.ctr_obj_init_args, self.ctr_obj_init_kwargs)

    @ctrl_object.setter
    def ctrl_object(self, val) -> None:
        self.__ctr_obj = val

    @property
    def func(self) -> Callable:
        return self.__func


class _WebsocketHandlerProxy(WebsocketHandler):

    def __init__(self) -> None:
        self.handshake: Callable = None
        self.open: Callable = None
        self.close: Callable = None
        self.ping: Callable = None
        self.pong: Callable = None
        self.text: Callable = None
        self.binary: Callable = None
        self.binary_frame: Callable = None

    async def await_func(self, obj):
        if asyncio.iscoroutine(obj):
            return await obj
        return obj

    async def on_handshake(self, *args, **kwargs):
        if self.handshake:
            return await self.await_func(self.handshake(*args, **kwargs))
        return None

    async def on_open(self, *args, **kwargs):
        if self.open:
            await self.await_func(self.open(*args, **kwargs))

    async def on_close(self, *args, **kwargs):
        if self.close:
            await self.await_func(self.close(*args, **kwargs))

    async def on_ping_message(self, *args, **kwargs):
        if self.ping:
            await self.await_func(self.ping(*args, **kwargs))
        else:
            return super().on_ping_message()

    async def on_pong_message(self, *args, **kwargs):
        if self.pong:
            await self.await_func(self.pong(*args, **kwargs))

    async def on_text_message(self, *args, **kwargs):
        if self.text:
            await self.await_func(self.text(*args, **kwargs))

    async def on_binary_message(self, *args, **kwargs):
        if self.binary:
            await self.await_func(self.binary(*args, **kwargs))

    async def on_binary_frame(self, *args, **kwargs):
        if self.binary_frame:
            return await self.await_func(self.binary_frame(*args, **kwargs))
        return super().on_binary_frame()


class _WebsocketHandlerClass:

    def __init__(self, url: str = "",
                 cls: Callable = None,
                 regexp: str = "",
                 singleton: bool = True
                 ) -> None:
        self.__url: str = url
        self.__regexp = regexp
        self.singleton: bool = singleton
        self.ctr_obj_init_args = None
        self.ctr_obj_init_kwargs = None
        self.__ctr_obj: object = None
        self.__cls: Callable = cls

    @property
    def _is_config_ok(self):
        try:
            assert self.url or self.regexp, "you should set one of url and regexp"
            assert not (
                self.url and self.regexp), "you can only set one of url and regexp, not both"
            assert self.cls is not None and inspect.isclass(self.cls)
            return True
        except AssertionError as ae:
            _logger.warn(
                f"[{self.__url}|{self.regexp}] => {self.cls} configurate error: {ae}")
            return False

    @property
    def url(self) -> str:
        return self.__url

    @property
    def regexp(self) -> str:
        return self.__regexp

    @property
    def cls(self) -> str:
        return self.__cls

    @property
    def ctrl_object(self) -> object:
        if not self.cls:
            return self.__ctr_obj
        if not self.singleton:
            obj = self._create_ctrl_obj()
            _logger.debug(f"singleton: create a object -> {obj}")
            return obj

        if self.__ctr_obj is None:
            self.__ctr_obj = self._create_ctrl_obj()
            _logger.debug(
                f"object does not exist, create one -> {self.__ctr_obj} ")
        else:
            _logger.debug(f"object[{self.__ctr_obj}] exists, return. ")
        return self.__ctr_obj

    def _create_ctrl_obj(self) -> object:
        return _create_object(self.cls, self.ctr_obj_init_args, self.ctr_obj_init_kwargs)

    @ctrl_object.setter
    def ctrl_object(self, val) -> None:
        self.__ctr_obj = val


__favicon = base64.b64decode(
    b'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFcklEQVRYR+1XW0wUZxT+Zm8gy2WBZRFYBBfDRhG1gkAaNYQY26ap6UPTpj409pK2SdOkTdq01SZNtPWhPhibpn00bWLs5cW2tiVpH7QaRRAERURRoEWEhXVBltvO7Mz0nH9m' +
    b'V9blYhOTxsQ/bHbn8p/znfN955tB0mnhf1zSIwCPOvDQdSAUDqMzPIO+6QgmFRUSzZDdakVRqg2V6Snw5mRDkqT7nqv7moLOkSCODo7j11FKrtqgwgpIljlJKCEBkfQoShHBDk8GXi7Oxvp895JAFgXQFgji4+5hNE5RfImSWigRF6ffU+Fc' +
    b'J+FrhE3SVTybruPgWi9KXJkLApkXgKZp2NfRi0+HZhG12oxqOQn3ezHbEp5G93JzBFAdLl3BUX8uniwtmBdEEoCoquLVpmv4diIWyGivWPzNSfjDPDOguaC4+linjJsBTUdKVMPxymxsK1meBCIJwJ7z17F/VANsZhIuRcTSKJCCI+XZcDms' +
    b'eK1rHP2ckJgRgAi4U1NJkDaM2+wGQFAc+mPKPGoEl7f64E5PSwCRAKBrNIQNzSNQrBzY5FtUTr8pwdtuG76oXiUCXBoJobZpGPVOBY2zDpST+D4rd8O9zIb6y+NEAwuVwXMXOIaKj/Js2F9l7I+tBACvnOnB4QlGrhqVxdtO5yjIH5UubPN6' +
    b'4pt/6LmFA91B9NLtNSnA8e0VmJmdReaJG9DtqSYAkzoK4FWm0P/UelhpbJMARKm9nt+uYsxBGzmbmDKTfJ0OKElHdS7WeXISKghMTOK91gEceKwQy11ZGAtPIvevvwkA02B2T0wNxYzKuFFbAJ87NxlAXzAE37nbRuu5bdaY8syRU3X86M/A' +
    b'c75kNbcPBlCUkYa8zAwc6uzHOwMRowCLOT0xAETj2Q1u1BXmJQO4NHIb61oZAO+k5GKUTMULIeposMn4s37tgk7XSRqqaR7GjJ2NgP3AoE4EIw2ARNqywYPq5XcNKq6BgdAYVpwLEAAWD+0RNLGKzWnghqhRfO1Nw5sVKxNoiB3sOHkFv0xT' +
    b'Mu6eKIRD8EYOpkGSVQQ2FyEv664xxQFohK7w904E7MsMAML1WMEEYo772UkrP68mYylJpGJschL5J/+BYifjEu03JSQmwDjwaxFceaIioYMJU7DrzFV8E+bEhp2KxHEAfGxQ4VIVNG3ywp/rineieWgUtReJQuZdeEBMwLyHqdDwPo3h51W+' +
    b'hO4lAOAgdRdC0FmIcd8XHmxSYe4lEHUWGacb1sRHqoWeGzUXgub8832sH9NLiH5LVEVXXT787uyFAfCVF09dwXdTVAULKVaFENI9S9FwZJUTO8uLxYUgPabzTw1AYwpiBiRayCam43mnhu+3+JPCJFlxIDyFqtM3MGhzJD5yzUaICJyAQFVp' +
    b'02jZvs7kVEdFYwe6JNKQoMHcQF9pkSguPl6IspyspQHwHa3DQTS0DWGCQcRmmbXAH6aGJ4WEJUVkXKe2+tyGOb10phud0yqG6EVl2EpAeFH1X3odeKuydJ42Ms4F/i84S3p4pmMEtyVyNKpiJz3bP1iVi74ZGXt7x9Em03kasZ/WZGLHSuMp' +
    b'92FbH06MTqLA6cCxMCe3YI9Hwr5NZQt6x6IvJD2hO3ih9RbaZ4HA1kKaX6OFsqLg3fO9+CokoWljDmoLDGPZ3daPY4NT+GR1NnZ13cFBnxNvVBQv+oq25CvZrKxgb3s/imwqXq8sg509nlZ4ehqHWrqwe8tGYskwncOt3XjaX4xro+PIcqah' +
    b'0pOo+Pk4WBJAbNOtOxNovjmEVHovsJAOUh0p2OxbQb+NJ1vPzUE4UtNQcs+YzUv8nJP3DWCxQIoiQ5ZlOJ3pS+VLuv5AAPznrA+6Aw81gH8BHb95v9r9AsoAAAAASUVORK5CYII='
)


def _favicon():
    return Headers({"Content-Type": "image/png"}), __favicon


class AppConf:

    def __init__(self) -> None:
        self._request_mappings: List[_ControllerFunction] = []
        self._request_clz_mapping: Dict[Any, _ControllerFunction] = {}

        self._filters = []

        self._ctrls = {}
        self._ctrl_singletons = {}

        self._ws_handlers: Dict[str, _WebsocketHandlerClass] = {}

        self._error_page = {}

        self._session_factory: HttpSessionFactory = None

        self.request_map("/favicon.ico")(_favicon)
        self.route = self.request_map
        self.model_binding_conf = ModelBindingConf()

    @property
    def session_factory(self):
        if self._session_factory == None:
            self._session_factory = LocalSessionFactory()
        return self._session_factory

    @session_factory.setter
    def session_factory(self, session_factory: HttpSessionFactory):
        self._session_factory = session_factory

    def model_binding(self, arg_type: Type):
        def map(model_binding_type):
            self.model_binding_conf.model_bingding_types[arg_type] = model_binding_type
            return model_binding_type
        return map

    def default_model_binding(self, *anno_args):
        def map(model_binding_type):
            self.model_binding_conf.default_model_binding_type = model_binding_type
            return model_binding_type
        if len(anno_args) == 1 and inspect.isclass(anno_args[0]):
            return map(anno_args[0])
        return map

    def controller(self, *anno_args, singleton: bool = True, args: List[Any] = [], kwargs: Dict[str, Any] = {}):
        def map(ctr_obj_class):
            _logger.debug(
                f"map controller[{ctr_obj_class}]({singleton}) with args: {args}, and kwargs: {kwargs})")
            self._ctrls[ctr_obj_class] = (singleton, args, kwargs)
            return ctr_obj_class
        if len(anno_args) == 1 and inspect.isclass(anno_args[0]):
            return map(anno_args[0])
        return map

    def request_map(self, *anno_args,
                    url: str = "",
                    regexp: str = "",
                    method: Union[str, list, tuple] = "",
                    headers: Union[str, list, tuple] = "",
                    match_all_headers_expressions: bool = None,
                    params: Union[str, list, tuple] = "",
                    match_all_params_expressions: bool = None) -> Callable:
        _url = url
        len_args = len(anno_args)
        assert len_args <= 1

        arg_ctrl = None
        if len_args == 1:
            if isinstance(anno_args[0], str):
                assert not url and not regexp
                _url = anno_args[0]
            elif callable(anno_args[0]):
                arg_ctrl = anno_args[0]

        def map(ctrl):
            if isinstance(method, list):
                mths = method
            elif isinstance(method, tuple):
                mths = list(method)
            else:
                mths = [m.strip() for m in method.split(',')]

            hs = _to_str_list(headers)
            ps = _to_str_list(params)

            if inspect.isclass(ctrl):
                self._request_clz_mapping[ctrl] = _ControllerFunction(url=_url,
                                                                      regexp=regexp,
                                                                      method=mths,
                                                                      headers=hs,
                                                                      match_all_headers_expressions=match_all_headers_expressions,
                                                                      params=ps,
                                                                      match_all_params_expressions=match_all_params_expressions,
                                                                      func=ctrl)

                return ctrl

            for mth in mths:
                cf = _ControllerFunction(url=_url,
                                         regexp=regexp,
                                         method=mth,
                                         headers=hs,
                                         match_all_headers_expressions=match_all_headers_expressions,
                                         params=ps,
                                         match_all_params_expressions=match_all_params_expressions,
                                         func=ctrl)
                _logger.debug(
                    f"map url {_url} with method[{mth}] to function {ctrl}. with headers {cf.headers} and params {cf.params}")
                self._request_mappings.append(cf)
            # return the original function, so you can use a decoration chain
            return ctrl

        if arg_ctrl:
            return map(arg_ctrl)
        else:
            return map

    def request_filter(self, path: str = "", regexp: str = ""):
        p = path
        r = regexp
        assert (p and not r) or (not p and r)

        def map(filter_fun):
            self._filters.append(
                {"path": p, "url_pattern": r, "func": filter_fun})
            return filter_fun
        return map

    def filter_map(self, regexp: str = "", filter_function: Callable = None) -> Callable:
        """ deprecated, plese request_filter instead. """
        def map(filter_fun):
            self._filters.append({"url_pattern": regexp, "func": filter_fun})
            return filter_fun
        if filter_function:
            map(filter_function)
        return map

    def _get_singletion(self, clz, args, kwargs):
        if clz not in self._ctrl_singletons:
            self._ctrl_singletons[clz] = _create_object(clz, args, kwargs)
        return self._ctrl_singletons[clz]

    def websocket_handler(self, endpoint: str = "", regexp: str = "", singleton: bool = True) -> Callable:
        def map(ws_class):
            self._ws_handlers[endpoint + ":::" + regexp] = _WebsocketHandlerClass(
                url=endpoint, regexp=regexp, cls=ws_class, singleton=singleton)
            return ws_class

        return map

    def websocket_handshake(self, endpoint: str = "", regexp: str = "") -> Callable:
        def map(ctrl):
            k = endpoint + ":::" + regexp
            if k not in self._ws_handlers:
                handler = _WebsocketHandlerClass(url=endpoint, regexp=regexp)
                handler.ctrl_object = _WebsocketHandlerProxy()
                handler.ctrl_object.handshake = ctrl
                self._ws_handlers[k] = handler
            else:
                handler = self._ws_handlers[k]
                if not handler.cls and isinstance(handler.ctrl_object, _WebsocketHandlerProxy):
                    handler.ctrl_object.handshake = ctrl
            return ctrl
        return map

    def websocket_open(self, endpoint: str = "", regexp: str = "") -> Callable:
        def map(ctrl):
            k = endpoint + ":::" + regexp
            if k not in self._ws_handlers:
                handler = _WebsocketHandlerClass(url=endpoint, regexp=regexp)
                handler.ctrl_object = _WebsocketHandlerProxy()
                handler.ctrl_object.open = ctrl
                self._ws_handlers[k] = handler
            else:
                handler = self._ws_handlers[k]
                if not handler.cls and isinstance(handler.ctrl_object, _WebsocketHandlerProxy):
                    handler.ctrl_object.open = ctrl
            return ctrl
        return map

    def websocket_close(self, endpoint: str = "", regexp: str = "") -> Callable:
        def map(ctrl):
            k = endpoint + ":::" + regexp
            if k not in self._ws_handlers:
                handler = _WebsocketHandlerClass(url=endpoint, regexp=regexp)
                handler.ctrl_object = _WebsocketHandlerProxy()
                handler.ctrl_object.close = ctrl
                self._ws_handlers[k] = handler
            else:
                handler = self._ws_handlers[k]
                if not handler.cls and isinstance(handler.ctrl_object, _WebsocketHandlerProxy):
                    handler.ctrl_object.close = ctrl
            return ctrl
        return map

    def websocket_message(self, endpoint: str = "", regexp: str = "", message_type: str = WEBSOCKET_MESSAGE_TEXT) -> Callable:
        def map(ctrl):
            k = endpoint + ":::" + regexp
            if k not in self._ws_handlers:
                handler = _WebsocketHandlerClass(url=endpoint, regexp=regexp)
                handler.ctrl_object = _WebsocketHandlerProxy()
                self._ws_handlers[k] = handler

            handler = self._ws_handlers[k]
            if not handler.cls and isinstance(handler.ctrl_object, _WebsocketHandlerProxy):
                if message_type == WEBSOCKET_MESSAGE_TEXT:
                    handler.ctrl_object.text = ctrl
                elif message_type == WEBSOCKET_MESSAGE_BINARY:
                    handler.ctrl_object.binary = ctrl
                elif message_type == WEBSOCKET_MESSAGE_BINARY_FRAME:
                    handler.ctrl_object.binary_frame = ctrl
                elif message_type == WEBSOCKET_MESSAGE_PING:
                    handler.ctrl_object.ping = ctrl
                elif message_type == WEBSOCKET_MESSAGE_PONG:
                    handler.ctrl_object.pong = ctrl
                else:
                    _logger.error(
                        f"Cannot match message type: [{message_type}]!")
            return ctrl
        return map

    def error_message(self, *anno_args):
        len_args = len(anno_args)
        arg_func = None

        if len_args == 1:
            if callable(anno_args[0]):
                arg_func = anno_args[0]

        if not arg_func:
            if anno_args:
                args = [it for it in anno_args]
            else:
                args = [""]
        else:
            args = [""]

        def map(func):
            for arg in args:
                self._error_page[arg] = func
            return func

        if arg_func:
            return map(arg_func)
        else:
            return map

    def _get_request_mappings(self) -> List[_ControllerFunction]:
        mappings: List[_ControllerFunction] = []

        for ctr_fun in self._request_mappings:
            clz = ctr_fun.clz
            if clz is not None and clz in self._request_clz_mapping:
                clz_ctrl = self._request_clz_mapping[clz]
                clz_url = clz_ctrl.url
                methods = clz_ctrl.method

                if ctr_fun.regexp:
                    full_url = ctr_fun.url
                else:
                    fun_url = ctr_fun.url
                    if fun_url:
                        if not clz_url.endswith("/"):
                            clz_url = clz_url + "/"
                        if fun_url.startswith("/"):
                            fun_url = fun_url[1:]
                        full_url = f"{clz_url}{fun_url}"
                    else:
                        full_url = clz_url

                hs = ctr_fun.headers + clz_ctrl.headers
                mhs = ctr_fun._match_all_headers_expressions if ctr_fun._match_all_headers_expressions is not None else clz_ctrl._match_all_headers_expressions
                ps = ctr_fun.params + clz_ctrl.params
                mps = ctr_fun._match_all_params_expressions if ctr_fun._match_all_params_expressions is not None else clz_ctrl._match_all_params_expressions
                if not ctr_fun.method and methods:
                    for mth in methods:
                        _logger.debug(
                            f"map url {full_url} included [{clz_url}] with method[{mth}] to function {ctr_fun.func}. ")
                        mappings.append(_ControllerFunction(url=full_url,
                                                            regexp=ctr_fun.regexp,
                                                            method=mth,
                                                            func=ctr_fun.func,
                                                            headers=hs,
                                                            match_all_headers_expressions=mhs,
                                                            params=ps,
                                                            match_all_params_expressions=mps))
                else:
                    _logger.debug(
                        f"map url {full_url} included [{clz_url}] with method[{ctr_fun.method}] to function {ctr_fun.func}. ")
                    mappings.append(_ControllerFunction(url=full_url,
                                                        regexp=ctr_fun.regexp,
                                                        method=ctr_fun.method,
                                                        func=ctr_fun.func,
                                                        headers=hs,
                                                        match_all_headers_expressions=mhs,
                                                        params=ps,
                                                        match_all_params_expressions=mps))
            else:
                mappings.append(ctr_fun)

        for ctr_fun in mappings:
            if not ctr_fun._is_config_ok:
                continue
            clz = ctr_fun.clz
            if clz is not None and clz in self._ctrls:
                singleton, args, kwargs = self._ctrls[clz]
                ctr_fun.singleton = singleton
                ctr_fun.ctr_obj_init_args = args
                ctr_fun.ctr_obj_init_kwargs = kwargs
                if singleton:
                    ctr_fun.ctrl_object = self._get_singletion(
                        clz, args, kwargs)

        return mappings

    def _get_filters(self):
        return self._filters

    def _get_websocket_handlers(self) -> List[_WebsocketHandlerClass]:
        return list(self._ws_handlers.values())

    def _get_error_pages(self) -> Dict[str, Callable]:
        return self._error_page


_default_app_conf = AppConf()
_app_confs: Dict[str, AppConf] = {}


def controller(*anno_args, singleton: bool = True, args: List[Any] = [], kwargs: Dict[str, Any] = {}):
    return _default_app_conf.controller(*anno_args, singleton=singleton, args=args, kwargs=kwargs)


def request_map(*anno_args,
                url: str = "",
                regexp: str = "",
                method: Union[str, list, tuple] = "",
                headers: Union[str, list, tuple] = "",
                match_all_headers_expressions: bool = None,
                params: Union[str, list, tuple] = "",
                match_all_params_expressions: bool = None) -> Callable:
    return _default_app_conf.request_map(*anno_args, url=url,
                                         regexp=regexp,
                                         method=method,
                                         headers=headers,
                                         match_all_headers_expressions=match_all_headers_expressions,
                                         params=params,
                                         match_all_params_expressions=match_all_params_expressions)


route = request_map


def request_filter(path: str = "", regexp: str = ""):
    return _default_app_conf.request_filter(path=path, regexp=regexp)


def filter_map(regexp: str = "", filter_function: Callable = None) -> Callable:
    """ deprecated, plese request_filter instead. """
    _logger.warning(
        "`filter_map` is deprecated and will be removed in future version,  please use `request_filter` instead. ")
    return _default_app_conf.filter_map(regexp=regexp, filter_function=filter_function)


def websocket_handler(endpoint: str = "", regexp: str = "", singleton: bool = True) -> Callable:
    return _default_app_conf.websocket_handler(endpoint=endpoint, regexp=regexp, singleton=singleton)


def websocket_handshake(endpoint: str = "", regexp: str = ""):
    return _default_app_conf.websocket_handshake(endpoint=endpoint, regexp=regexp)


def websocket_open(endpoint: str = "", regexp: str = ""):
    return _default_app_conf.websocket_open(endpoint=endpoint, regexp=regexp)


def websocket_close(endpoint: str = "", regexp: str = ""):
    return _default_app_conf.websocket_close(endpoint=endpoint, regexp=regexp)


def websocket_message(endpoint: str = "", regexp: str = "", message_type: str = WEBSOCKET_MESSAGE_TEXT):
    return _default_app_conf.websocket_message(endpoint=endpoint, regexp=regexp, message_type=message_type)


def error_message(*anno_args):
    return _default_app_conf.error_message(*anno_args)


def model_binding(arg_type: Type):
    return _default_app_conf.model_binding(arg_type)


def default_model_binding(*anno_args):
    return _default_app_conf.default_model_binding(*anno_args)


def set_session_factory(session_factory: HttpSessionFactory):
    _default_app_conf.session_factory = session_factory


def get_app_conf(tag: str = "") -> AppConf:
    if not tag:
        return _default_app_conf
    if tag not in _app_confs:
        _app_confs[tag] = AppConf()
    return _app_confs[tag]

# file: utils/py
# -*- coding: utf-8 -*-
import inspect
import time
import os
import email
import json
import re
from collections import OrderedDict
from typing import Any, Tuple, Union
from urllib.parse import unquote, quote
# from ..models import HttpError, StaticFile, DEFAULT_ENCODING

# from .logger import get_logger


# _logger = get_logger("naja_atra.utils.http_utils")


def remove_url_first_slash(url: str):
    _url = url
    while _url.startswith("/"):
        _url = url[1:]
    return _url


def get_function_args(func, default_type=str):
    argspec = inspect.getfullargspec(func)
    # ignore first argument like `self` or `clz` in object methods or class methods
    start = 1 if inspect.ismethod(func) else 0
    if argspec.defaults is None:
        args = argspec.args[start:]
    else:
        args = argspec.args[start: len(argspec.args) - len(argspec.defaults)]
    arg_turples = []
    for arg in args:
        if arg in argspec.annotations:
            ty = argspec.annotations[arg]
        else:
            ty = default_type
        arg_turples.append((arg, ty))
    return arg_turples


def get_function_kwargs(func, default_type=str):
    argspec = inspect.getfullargspec(func)
    if argspec.defaults is None:
        return []

    kwargs = OrderedDict(
        zip(argspec.args[-len(argspec.defaults):], argspec.defaults))
    kwarg_turples = []
    for k, v in kwargs.items():
        if k in argspec.annotations:
            k_anno = argspec.annotations[k]
        else:
            k_anno = default_type
        kwarg_turples.append((k, v, k_anno))
    return kwarg_turples


def break_into(txt: str, separator: str):
    try:
        idx = txt.index(separator)
        return txt[0: idx], txt[idx + len(separator):]
    except ValueError:
        return txt, None


def put_to(params, key, val):
    if key not in params.keys():
        params[key] = [val]
    else:
        params[key].append(val)


def decode_query_string(query_string: str):
    params = {}
    if not query_string:
        return params
    pairs = query_string.split("&")
    for item in pairs:
        key, val = break_into(item, "=")
        if val is None:
            val = ""
        put_to(params, unquote(key), unquote(val))

    return params


def get_charset(s: str) -> str:
    pattern = re.compile(r"charset=([^;]+)")
    m = pattern.search(s)
    if m is not None:
        return m.group(1)
    else:
        return DEFAULT_ENCODING


def date_time_string(timestamp=None):
    if timestamp is None:
        timestamp = time.time()
    return email.utils.formatdate(timestamp, usegmt=True)


def decode_response_body(raw_body: Any) -> Tuple[str, Union[str,  bytes, StaticFile]]:
    content_type = "text/plain; chartset=utf8"
    if raw_body is None:
        body = ""
    elif isinstance(raw_body, dict):
        content_type = "application/json; charset=utf8"
        body = json.dumps(raw_body, ensure_ascii=False)
    elif isinstance(raw_body, str):
        body = raw_body.strip()
        if body.startswith("<?xml") and body.endswith(">"):
            content_type = "text/xml; charset=utf8"
        elif body.lower().startswith("<!doctype html") and body.endswith(">"):
            content_type = "text/html; charset=utf8"
        elif body.lower().startswith("<html") and body.endswith(">"):
            content_type = "text/html; charset=utf8"
        else:
            content_type = "text/plain; charset=utf8"
    elif isinstance(raw_body, StaticFile):
        if not os.path.isfile(raw_body.file_path):
            _logger.error(
                f"Cannot find file[{raw_body.file_path}] specified in StaticFile body.")
            raise HttpError(404, explain="Cannot find file for this url.")
        else:
            body = raw_body
            content_type = body.content_type
    elif isinstance(raw_body, bytes):
        body = raw_body
        content_type = "application/octet-stream"
    else:
        body = raw_body
    return content_type, body


def decode_response_body_to_bytes(raw_body: Any) -> Tuple[str, bytes]:
    content_type, body = decode_response_body(raw_body)
    if body is None:
        byte_body = b''
    elif isinstance(body, str):
        byte_body = body.encode(DEFAULT_ENCODING, 'replace')
    elif isinstance(body, bytes):
        byte_body = body
    elif isinstance(body, StaticFile):
        with open(body.file_path, "rb") as in_file:
            byte_body = in_file.read()
    else:
        raise HttpError(400, explain="Cannot read body into bytes!")
    return content_type, byte_body


def get_path_reg_pattern(url):
    _url: str = url
    path_names = re.findall("(?u)\\{\\w+\\}", _url)
    if len(path_names) == 0:
        if _url.startswith("**"):
            _url = _url[2:]
            assert _url.find(
                "*") < 0, "You can only config a * or ** at the start or end of a path."
            _url = f'^([\\w%.\\-@!\\(\\)\\[\\]\\|\\$/]+){_url}$'
            return _url, [quote("__path_wildcard")]
        elif _url.startswith("*"):
            _url = _url[1:]
            assert _url.find(
                "*") < 0, "You can only config a * or ** at the start or end of a path."
            _url = f'^([\\w%.\\-@!\\(\\)\\[\\]\\|\\$]+){_url}$'
            return _url, [quote("__path_wildcard")]
        elif _url.endswith("**"):
            _url = _url[0: -2]
            assert _url.find(
                "*") < 0, "You can only config a * or ** at the start or end of a path."
            _url = f'^{_url}([\\w%.\\-@!\\(\\)\\[\\]\\|\\$/]+)$'
            return _url, [quote("__path_wildcard")]
        elif _url.endswith("*"):
            _url = _url[0: -1]
            assert _url.find(
                "*") < 0, "You can only config a * or ** at the start or end of a path."
            _url = f'^{_url}([\\w%.\\-@!\\(\\)\\[\\]\\|\\$]+)$'
            return _url, [quote("__path_wildcard")]
        else:
            # normal url
            return None, path_names
    for name in path_names:
        _url = _url.replace(name, "([\\w%.\\-@!\\(\\)\\[\\]\\|\\$]+)")
    _url = f"^{_url}$"

    quoted_names = []
    for name in path_names:
        name = name[1: -1]
        quoted_names.append(quote(name))
    return _url, quoted_names

# file: http_servers/routing_server.py
# -*- coding: utf-8 -*-

from abc import abstractmethod
import json
import os
import re
from urllib.parse import unquote
from typing import Any, Callable, Dict, List, Set, Tuple, Union

# from ..models import StaticFile, HttpSessionFactory
# from ..request_handlers.model_bindings import ModelBindingConf
# from ..app_conf import _WebsocketHandlerClass, _ControllerFunction
# from ..utils.http_utils import remove_url_first_slash, get_function_args, get_function_kwargs, get_path_reg_pattern
# from ..utils.logger import get_logger


_EXT_CONTENT_TYPE = {
    ".html": "text/html",
    ".htm": "text/html",
    ".xhtml": "text/html",
    ".css": "text/css",
    ".js": "text/javascript",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".ico": "image/x-icon",
    ".svg": "image/svg+xml",
    ".gif": "image/gif",
    ".avif": "image/avif",
    ".avifs": "image/avif",
    ".webp": "image/webp",
    ".pdf": "application/pdf",
    ".json": "application/json",
    ".mp4": "video/mp4",
    ".mp3": "video/mp3",
    ".txt": "text/plain"
}


class RoutingServer:

    HTTP_METHODS = ["OPTIONS", "GET", "HEAD",
                    "POST", "PUT", "DELETE", "TRACE", "CONNECT"]

    def __init__(self, res_conf={}, model_binding_conf: ModelBindingConf = ModelBindingConf()):
        self.method_url_mapping: Dict[str,
                                      Dict[str, List[_ControllerFunction]]] = {"_": {}}
        self.path_val_url_mapping: Dict[str, Dict[str, List[Tuple[_ControllerFunction, List[str]]]]] = {
            "_": {}}
        self.method_regexp_mapping: Dict[str, Dict[str, List[_ControllerFunction]]] = {
            "_": {}}
        for mth in self.HTTP_METHODS:
            self.method_url_mapping[mth] = {}
            self.path_val_url_mapping[mth] = {}
            self.method_regexp_mapping[mth] = {}

        self.filter_mapping = {}
        self._res_conf = []
        self.add_res_conf(res_conf)

        self.ws_mapping: Dict[str, _ControllerFunction] = {}
        self.ws_path_val_mapping: Dict[str, _ControllerFunction] = {}
        self.ws_regx_mapping: Dict[str, _ControllerFunction] = {}

        self.error_page_mapping = {}
        self.keep_alive = True
        self.__connection_idle_time: float = 60
        self.__keep_alive_max_request: int = 10
        self.session_factory: HttpSessionFactory = None
        self.model_binding_conf = model_binding_conf
        self.gzip_content_types: Set[str] = set()
        self.gzip_compress_level = 9

    @property
    def connection_idle_time(self):
        return self.__connection_idle_time

    @connection_idle_time.setter
    def connection_idle_time(self, val: float):
        if (isinstance(val, float) or isinstance(val, int)) and val > 0:
            self.__connection_idle_time = val

    @property
    def keep_alive_max_request(self):
        return self.__keep_alive_max_request

    @keep_alive_max_request.setter
    def keep_alive_max_request(self, val):
        if isinstance(val, int) and val > 0:
            self.__keep_alive_max_request = val

    def extend_gzip_content_types(self, content_types: Union[Set[str], List[str]]):
        for ctype in content_types:
            self.gzip_content_types.add(ctype.lower())

    def put_to_method_url_mapping(self, method, url, ctrl):
        if url not in self.method_url_mapping[method]:
            self.method_url_mapping[method][url] = []
        self.method_url_mapping[method][url].insert(0, ctrl)

    def put_to_path_val_url_mapping(self, method, path_pattern, ctrl, path_names):
        if path_pattern not in self.path_val_url_mapping[method]:
            self.path_val_url_mapping[method][path_pattern] = []
        self.path_val_url_mapping[method][path_pattern].insert(
            0, (ctrl, path_names))

    def put_to_method_regexp_mapping(self, method, regexp, ctrl):
        if regexp not in self.method_regexp_mapping[method]:
            self.method_regexp_mapping[method][regexp] = []
        self.method_regexp_mapping[method][regexp].insert(0, ctrl)

    @property
    def res_conf(self):
        return self._res_conf

    @res_conf.setter
    def res_conf(self, val: Dict[str, str]):
        self._res_conf.clear()
        self.add_res_conf(val)

    def add_res_conf(self, val: Dict[str, str]):
        if not val or not isinstance(val, dict):
            return
        for k, v in val.items():
            res_k = k
            if not res_k.startswith("*") and res_k.endswith('/'):
                # xxx/ equals xxx/*
                res_k = res_k + "*"
            if res_k.startswith('*'):
                suffix = res_k[2:] if res_k.startswith("**") else res_k[1:]
                assert suffix.find('/') < 0 and suffix.find(
                    '*') < 0, "If a resource path starts with *, only suffix can be configurated. "
            if res_k.startswith('**.'):
                # **.xxx
                suffix = res_k[3:]
                key = f'^[\\w%.\\-@!\\(\\)\\[\\]\\|\\$/]+\\.{suffix}$'
            elif res_k.startswith('*.'):
                # *.xxx
                suffix = res_k[2:]
                key = f'^[\\w%.\\-@!\\(\\)\\[\\]\\|\\$]+\\.{suffix}$'
            elif res_k.endswith("/**"):
                # xx/**
                prefix = res_k[0:-2]
                while prefix.startswith('/'):
                    prefix = prefix[1:]
                assert prefix.find(
                    "*") < 0, "You can only config a * or ** at the start or end of a path."
                key = f'^{prefix}([\\w%.\\-@!\\(\\)\\[\\]\\|\\$/]+)$'
            elif res_k.endswith("/*"):
                # xx/*
                prefix = res_k[0:-1]
                while prefix.startswith('/'):
                    prefix = prefix[1:]
                assert prefix.find(
                    "*") < 0, "You can only config a * or ** at the start or end of a path."
                key = f'^{prefix}([\\w%.\\-@!\\(\\)\\[\\]\\|\\$]+)$'

            if v.endswith(os.path.sep):
                val = v
            else:
                val = v + os.path.sep
            self._res_conf.append((key, val))

    def map_controller(self, ctrl: _ControllerFunction):
        url = ctrl.url
        regexp = ctrl.regexp
        method = ctrl.method
        _logger.debug(
            f"map url {url}|{regexp} with method::{method}, headers::{ctrl.headers} and params::{ctrl.params} to function {ctrl.func}. ")
        assert method is None or method == "" or method.upper() in self.HTTP_METHODS
        _method = method.upper() if method is not None and method != "" else "_"
        if regexp:
            self.put_to_method_regexp_mapping(_method, regexp, ctrl)
        else:
            _url = remove_url_first_slash(url)

            path_pattern, path_names = get_path_reg_pattern(_url)
            if path_pattern is None:
                self.put_to_method_url_mapping(_method, _url, ctrl)
            else:
                self.put_to_path_val_url_mapping(
                    _method, path_pattern, ctrl, path_names)

    def _res_(self, fpath: str):
        fext = os.path.splitext(fpath)[1]
        ext = fext.lower()
        content_type = _EXT_CONTENT_TYPE.get(ext, "application/octet-stream")
        return StaticFile(fpath, content_type)

    def get_url_controllers(self, path: str = "", method: str = "") -> List[Tuple[_ControllerFunction, Dict, List]]:
        # explicitly url matching
        if path in self.method_url_mapping[method]:
            return [(ctrl, {}, ()) for ctrl in self.method_url_mapping[method][path]]
        elif path in self.method_url_mapping["_"]:
            return [(ctrl, {}, ()) for ctrl in self.method_url_mapping["_"][path]]

        # url with path value matching
        path_val_res = self.__try_get_from_path_val(path, method)
        if path_val_res is None:
            path_val_res = self.__try_get_from_path_val(path, "_")
        if path_val_res is not None:
            return path_val_res

        # regexp
        regexp_res = self.__try_get_ctrl_from_regexp(path, method)
        if regexp_res is None:
            regexp_res = self.__try_get_ctrl_from_regexp(path, "_")
        if regexp_res is not None:
            return regexp_res
        # static files
        for k, v in self.res_conf:
            match_static_path_conf = re.match(k, path)
            _logger.debug(
                f"{path} macth static file conf {k} ? {match_static_path_conf}")
            if match_static_path_conf:
                if match_static_path_conf.groups():
                    fpath = f"{v}{match_static_path_conf.group(1)}"
                else:
                    fpath = f"{v}{path}"

                def static_fun():
                    return self._res_(fpath)
                return [(_ControllerFunction(func=static_fun), {}, ())]
        return []

    def __try_get_ctrl_from_regexp(self, path, method):
        for regex, ctrls in self.method_regexp_mapping[method].items():
            m = re.match(regex, f"/{path}") or re.match(regex, path)
            _logger.debug(
                f"regexp::pattern::[{regex}] => path::[{path}] match? {m is not None}")
            if m:
                res = []
                grps = tuple([unquote(v) for v in m.groups()])
                for ctrl in ctrls:
                    res.append((ctrl, [], grps))
                return res
        return None

    def __try_get_from_path_val(self, path, method):
        for patterns, val in self.path_val_url_mapping[method].items():
            m = re.match(patterns, path)
            _logger.debug(
                f"url with path value::pattern::[{patterns}] => path::[{path}] match? {m is not None}")
            if m:
                res = []
                for ctrl_fun, path_names in val:
                    path_values = {}
                    for idx in range(len(path_names)):
                        key = unquote(path_names[idx])
                        path_values[key] = unquote(m.groups()[idx])
                    res.append((ctrl_fun, path_values, ()))
                return res
        return None

    def map_filter(self, filter_conf: Dict[str, Any]):
        # {"path": p, "url_pattern": r, "func": filter_fun}
        path = filter_conf["path"] if "path" in filter_conf else ""
        regexp = filter_conf["url_pattern"]
        filter_fun = filter_conf["func"]
        if path:
            regexp = get_path_reg_pattern(path)[0]
            if not regexp:
                regexp = f"^{path}$"
        _logger.debug(
            f"[path: {path}] map url regexp {regexp} to function: {filter_fun}")
        self.filter_mapping[regexp] = filter_fun

    def get_matched_filters(self, path):
        return self._get_matched_filters(remove_url_first_slash(path)) + self._get_matched_filters(path)

    def _get_matched_filters(self, path):
        available_filters = []
        for regexp, val in self.filter_mapping.items():
            m = re.match(regexp, path)
            _logger.debug(
                f"filter:: [{regexp}], path:: [{path}] match? {m is not None}")
            if m:
                available_filters.append(val)
        return available_filters

    def map_websocket_handler(self, handler: _WebsocketHandlerClass):
        url = handler.url
        regexp = handler.regexp
        _logger.debug(
            f"map url {url}|{regexp} to controller class {handler.cls}")
        if regexp:
            self.ws_regx_mapping[regexp] = handler
        else:
            url = remove_url_first_slash(url)
            path_pattern, path_names = get_path_reg_pattern(url)
            if path_pattern is None:
                self.ws_mapping[url] = handler
            else:
                self.ws_path_val_mapping[path_pattern] = (handler, path_names)

    def get_websocket_handler(self, path):
        # explicitly mapping
        if path in self.ws_mapping:
            return self.ws_mapping[path], {}, ()

        # path value mapping
        handler, path_vals = self.__try_get_ws_handler_from_path_val(path)
        if handler is not None:
            return handler, path_vals, ()
        # regexp mapping
        return self.__try_get_ws_hanlder_from_regexp(path)

    def __try_get_ws_hanlder_from_regexp(self, path):
        for regex, handler in self.ws_regx_mapping.items():
            m = re.match(regex, f"/{path}") or re.match(regex, path)
            _logger.debug(
                f"regexp::pattern::[{regex}] => path::[{path}] match? {m is not None}")
            if m:
                return handler, {}, tuple([unquote(v) for v in m.groups()])
        return None, {}, ()

    def __try_get_ws_handler_from_path_val(self, path):
        for patterns, val in self.ws_path_val_mapping.items():
            m = re.match(patterns, path)
            _logger.debug(
                f"websocket endpoint with path value::pattern::[{patterns}] => path::[{path}] match? {m is not None}")
            if m:
                handler, path_names = val
                path_values = {}
                for idx in range(len(path_names)):
                    key = unquote(path_names[idx])
                    path_values[key] = unquote(m.groups()[idx])
                return handler, path_values
        return None, {}

    def map_error_page(self, code: str, error_page_fun: Callable):
        if not code:
            c = "_"
        else:
            c = str(code).lower()
        self.error_page_mapping[c] = error_page_fun

    def _default_error_page(self, code: int, message: str = "", explain: str = ""):
        return json.dumps({
            "code": code,
            "message": message,
            "explain": explain
        })

    def error_page(self, code: int, message: str = "", explain: str = ""):
        c = str(code)
        func = None
        if c in self.error_page_mapping:
            func = self.error_page_mapping[c]
        elif code > 200:
            c0x = c[0:2] + "x"
            if c0x in self.error_page_mapping:
                func = self.error_page_mapping[c0x]
            elif "_" in self.error_page_mapping:
                func = self.error_page_mapping["_"]

        if not func:
            func = self._default_error_page
        _logger.debug(f"error page function:: {func}")

        co = code
        msg = message
        exp = explain

        args_def = get_function_args(func, None)
        kwargs_def = get_function_kwargs(func, None)

        args = []
        for n, t in args_def:
            _logger.debug(f"set value to error_page function -> {n}")
            if co is not None:
                if t is None or t == int:
                    args.append(co)
                    co = None
                    continue
            if msg is not None:
                if t is None or t == str:
                    args.append(msg)
                    msg = None
                    continue
            if exp is not None:
                if t is None or t == str:
                    args.append(exp)
                    exp = None
                    continue
            args.append(None)

        kwargs = {}
        for n, v, t in kwargs_def:
            if co is not None:
                if (t is None and isinstance(v, int)) or t == int:
                    kwargs[n] = co
                    co = None
                    continue
            if msg is not None:
                if (t is None and isinstance(v, str)) or t == str:
                    kwargs[n] = msg
                    msg = None
                    continue
            if exp is not None:
                if (t is None and isinstance(v, str)) or t == str:
                    kwargs[n] = exp
                    exp = None
                    continue
            kwargs[n] = v

        if args and kwargs:
            return func(*args, **kwargs)
        elif args:
            return func(*args)
        elif kwargs:
            return func(**kwargs)
        else:
            return func()

    @abstractmethod
    def start(self):
        pass

    @abstractmethod
    async def start_async(self):
        pass

    @abstractmethod
    def shutdown(self):
        pass

# file: request_handlers/http_session_local_impl.py
# -*- coding: utf-8 -*-
import threading
import uuid
import time

from typing import Any, Dict, List, Tuple
from threading import RLock
# from ..models import HttpSession, HttpSessionFactory
# from ..utils.logger import get_logger

# _logger = get_logger("naja_atra.request_handlers.http_session_local_impl")

_SESSION_TIME_CLEANING_INTERVAL = 60


def _get_from_dict(adict: Dict[str, Any], key: str) -> Any:
    if key not in adict:
        return None
    try:
        return adict[key]
    except KeyError:
        _logger.debug("key %s was deleted in other thread.")
        return None


class LocalSessionHolder:

    def __init__(self):
        self.__sessions: Dict[str, HttpSession] = {}
        self.__session_lock = RLock()
        self.__started = False
        self.__clearing_thread = threading.Thread(target=self._clear_time_out_session_in_bg, daemon=True)

    def _start_cleaning(self):
        if not self.__started:
            self.__started = True
            self.__clearing_thread.start()

    def _clear_time_out_session_in_bg(self):
        while True:
            time.sleep(_SESSION_TIME_CLEANING_INTERVAL)
            self._clear_time_out_session()

    def _clear_time_out_session(self):
        timeout_sessions: List[HttpSession] = []
        for v in self.__sessions.values():
            if not v.is_valid:
                timeout_sessions.append(v)
        for session in timeout_sessions:
            session.invalidate()

    def clean_session(self, session_id: str):
        if session_id in self.__sessions:
            try:
                _logger.debug("session[#%s] is being cleaned" % session_id)
                sess = self.__sessions[session_id]
                if not sess.is_valid:
                    del self.__sessions[session_id]
            except KeyError:
                _logger.debug("Session[#%s] in session cache is already deleted. " % session_id)

    def get_session(self, session_id: str) -> HttpSession:
        if not session_id:
            return None
        sess: HttpSession = _get_from_dict(self.__sessions, session_id)
        if sess and sess.is_valid:
            return sess
        else:
            return None

    def cache_session(self,  session: HttpSession):
        if not session:
            return None
        sess: HttpSession = _get_from_dict(self.__sessions, session.id)

        if sess:
            if session is sess:
                return
            sess.invalidate()
        with self.__session_lock:
            self.__sessions[session.id] = session
            self._start_cleaning()

            return session


class LocalSessionImpl(HttpSession):

    def __init__(self, id: str, creation_time: float, session_holder: LocalSessionHolder):
        super().__init__()
        self.__id = id
        self.__creation_time = creation_time
        self.__last_accessed_time = creation_time
        self.__is_new = True
        self.__attr_lock = RLock()
        self.__attrs = {}
        self.__session_holder = session_holder

    @property
    def id(self) -> str:
        return self.__id

    @property
    def creation_time(self) -> float:
        return self.__creation_time

    @property
    def last_accessed_time(self) -> float:
        return self.__last_accessed_time

    @property
    def is_new(self) -> bool:
        return self.__is_new

    def _set_last_accessed_time(self, last_acessed_time: float):
        self.__last_accessed_time = last_acessed_time
        self.__is_new = False

    @property
    def attribute_names(self) -> Tuple:
        return tuple(self.__attrs.keys())

    def get_attribute(self, name: str) -> Any:
        return _get_from_dict(self.__attrs, name)

    def set_attribute(self, name: str, value: Any) -> None:
        with self.__attr_lock:
            self.__attrs[name] = value

    def invalidate(self) -> None:
        self._set_last_accessed_time(0)
        self.__session_holder.clean_session(session_id=self.id)


class LocalSessionFactory(HttpSessionFactory):

    def __init__(self):
        self.__session_holder = LocalSessionHolder()
        self.__session_lock = RLock()

    def _create_local_session(self, session_id: str) -> HttpSession:
        if session_id:
            sid = session_id
        else:
            sid = uuid.uuid4().hex
        return LocalSessionImpl(sid, time.time(), self.__session_holder)

    def get_session(self, session_id: str, create: bool = False) -> HttpSession:
        sess: LocalSessionImpl = self.__session_holder.get_session(session_id)
        if sess:
            sess._set_last_accessed_time(time.time())
            return sess
        if not create:
            return None
        with self.__session_lock:
            session = self._create_local_session(session_id)
            self.__session_holder.cache_session(session)
            return session

# file: request_handlers/http_controller_handler.py
# -*- coding: utf-8 -*-
import asyncio
from asyncio.streams import StreamReader, StreamWriter
import gzip
import os
import json
import threading
import http.cookies as cookies
import datetime

from urllib.parse import unquote
from typing import Any, Callable, Dict, List, Tuple, Union

# from .model_bindings import ModelBindingConf
# from ..http_servers.routing_server import RoutingServer

# from ..models import FilterContext, HttpError, RequestBodyReader, StaticFile, Headers, Redirect, Response, Cookies, MultipartFile, Request, HttpSession, HttpSessionFactory
# from ..models import DEFAULT_ENCODING, SESSION_COOKIE_NAME
# from ..app_conf import _ControllerFunction
# from ..utils import http_utils

# from .http_session_local_impl import LocalSessionFactory
# from ..utils.logger import get_logger
# from ..utils.http_utils import get_function_args, get_function_kwargs


class RequestBodyReaderDumpWrapper(RequestBodyReader):

    def __init__(self, reader: StreamReader) -> None:
        self._reader = reader

    async def read(self, n: int = -1) -> bytes:
        _logger.warning(
            "`Content-Length` and `Transfer-Encoding` are both not set in request, so we can't read the body.")
        return b''

    async def read_to_end(self) -> bytes:
        _logger.warning(
            "`Content-Length` and `Transfer-Encoding` are both not set in request, so we can't read the body.")
        raise b''


class RequestBodyReaderWrapper(RequestBodyReader):

    def __init__(self, reader: StreamReader, content_length: int) -> None:
        if content_length is None or content_length < 0:
            raise HttpError(400, "Invalid content length")
        self._content_length: int = content_length
        self._remain_length: int = content_length
        self._reader: StreamReader = reader
        self._lock = asyncio.Lock()

    async def read(self, n: int = -1) -> bytes:
        async with self._lock:
            if not n or self._remain_length <= 0:
                return b''
            if n < 0:
                data = await self._reader.read(self._remain_length)
            else:
                data = await self._reader.read(min(n, self._remain_length))

            self._remain_length -= len(data)

            return data

    async def read_to_end(self) -> bytes:
        data = b''
        while self._remain_length > 0:
            data += await self.read()
        return data


class RequestBodyReaderChunkedReader(RequestBodyReader):

    def __init__(self, reader: StreamReader) -> None:
        self._reader = reader
        self._current_chunk_len: int = -1
        self._buffer: bytes = b''
        self._eof: bool = False
        self._lock = asyncio.Lock()

    async def _fill_buffer(self):
        if self._eof:
            _logger.warning("The body or this request has been all read. ")
            return
        if self._buffer:
            _logger.debug(
                f"There is still data in buffer. length: {len(self._buffer)}")
            return

        if self._current_chunk_len > 0:
            self._buffer = await self._reader.read(self._current_chunk_len)
            self._current_chunk_len -= len(self._buffer)
            return

        if self._current_chunk_len == 0:
            # Read \r\n at the end of a chunk
            rn = await self._reader.read(2)
            if rn != b'\r\n':
                raise HttpError(400, f"Invalid chunk end: {rn}")

        chunk_len = await self._reader.readline()
        chunk_len = chunk_len.rstrip(b'\r\n')
        self._current_chunk_len = int(chunk_len, 16)
        _logger.debug(f"current chunk length: {self._current_chunk_len}")
        if self._current_chunk_len == 0:
            # Read \r\n at the end of the final chunk
            rn = await self._reader.read(2)
            if rn != b'\r\n':
                raise HttpError(400, f"Invalid chunk end: {rn}")
            self._eof = True
            return
        if self._current_chunk_len < 0:
            raise HttpError(400, "Invalid chunk length")
        self._buffer = await self._reader.read(self._current_chunk_len)
        self._current_chunk_len -= len(self._buffer)

    async def read(self, n: int = -1) -> bytes:
        async with self._lock:
            if not n:
                return b''
            await self._fill_buffer()
            if n < 0 or n >= len(self._buffer):
                data = self._buffer
                self._buffer = b''
                return data
            else:
                data = self._buffer[:n]
                self._buffer = self._buffer[n:]
                return data

    async def read_to_end(self) -> bytes:
        data = b''
        while not self._eof:
            data += await self.read()
        return data


class RequestWrapper(Request):

    def __init__(self):
        super().__init__()
        self._headers_keys_in_lowcase = {}
        self._path = ""
        self.__session = None
        self._socket_req = None
        self._coroutine_objects = []
        self._session_fac: HttpSessionFactory = None

    @property
    def host(self) -> str:
        if "host" not in self._headers_keys_in_lowcase:
            return ""
        else:
            return self.headers["host"]

    @property
    def content_type(self) -> str:
        if "content-type" not in self._headers_keys_in_lowcase:
            return ""
        else:
            return self.headers["content-type"]

    @property
    def content_length(self) -> int:
        if "content-length" not in self._headers_keys_in_lowcase:
            return ""
        else:
            return self.headers["content-length"]

    def get_session(self, create: bool = False) -> HttpSession:
        if not self.__session:
            sid = self.cookies[SESSION_COOKIE_NAME].value if SESSION_COOKIE_NAME in self.cookies.keys(
            ) else ""
            session_fac = self._session_fac or LocalSessionFactory()
            self.__session = session_fac.get_session(sid, create)
        return self.__session

    def _put_coroutine_task(self, coroutine_object):
        self._coroutine_objects.append(coroutine_object)


class ResponseWrapper(Response):
    """ """

    def __init__(self, handler,
                 status_code=200,
                 headers=None):
        super().__init__(status_code=status_code, headers=headers, body="")
        self.__req_handler = handler
        self.__is_sent = False
        self.__header_sent = False
        self.__send_lock__ = threading.Lock()

    @property
    def is_sent(self) -> bool:
        return self.__is_sent

    def send_error(self, status_code: int, message: str = "", explain: str = "") -> None:
        with self.__send_lock__:
            self.__is_sent = True
            self.status_code = status_code
            self.__req_handler.send_error(
                self.status_code, message=message, explain=explain, headers=self.headers)

    def send_redirect(self, url: str) -> None:
        self.status_code = 302
        self.set_header("Location", url)
        self.body = None
        self.send_response()

    def send_response(self) -> None:
        with self.__send_lock__:
            self.__send_response()

    def __send_response(self):
        assert not self.__is_sent and not self.__header_sent, "This response has benn sent"
        self.__header_sent = True
        self.__is_sent = True
        self.__req_handler._send_response({
            "status_code": self.status_code,
            "headers": self.headers,
            "cookies": self.cookies,
            "body": self.body
        })

    def __send_headers(self):
        if not self.__header_sent:
            self.__header_sent = True
            self.__req_handler._send_and_end_res_headers(
                self.status_code, headers=self.headers, cks=self.cookies)

    def write_bytes(self, data: bytes):
        assert not self.__is_sent, "This response has benn sent"
        assert isinstance(data, bytes) or isinstance(
            data, bytearray), "You can "
        self.__send_headers()
        self.__req_handler.writer.write(data)

    def close(self):
        self.__is_sent = True
        self.__req_handler.writer.write_eof()
        self.__req_handler.writer.close()


class FilterContextImpl(FilterContext):
    """Context of a filter"""

    DEFAULT_TIME_OUT = 10

    def __init__(self, req, res, controller: _ControllerFunction, model_binding_conf: ModelBindingConf, filters: List[Callable] = None):
        self.__request: RequestWrapper = req
        self.__response = res
        self.__controller: _ControllerFunction = controller
        self.__filters: List[Callable] = filters if filters is not None else []
        self.__model_binding_conf = model_binding_conf

    @property
    def request(self) -> RequestWrapper:
        return self.__request

    @property
    def response(self) -> ResponseWrapper:
        return self.__response

    async def _run_ctrl_fun(self):
        args = await self.__prepare_args()
        kwargs = await self.__prepare_kwargs()
        if asyncio.iscoroutinefunction(self.__controller.func):
            if kwargs is None:
                ctr_res = await self.__controller.func(*args)
            else:
                ctr_res = await self.__controller.func(*args, **kwargs)
        else:
            if kwargs is None:
                ctr_res = self.__controller.func(*args)
            else:
                ctr_res = self.__controller.func(*args, **kwargs)
        return ctr_res

    def _do_res(self, ctr_res):
        session = self.request.get_session()
        if session and session.is_valid:
            exp = datetime.datetime.utcfromtimestamp(
                session.last_accessed_time + session.max_inactive_interval)
            sck = Cookies()
            sck[SESSION_COOKIE_NAME] = session.id
            sck[SESSION_COOKIE_NAME]["httponly"] = True
            sck[SESSION_COOKIE_NAME]["path"] = "/"
            sck[SESSION_COOKIE_NAME]["expires"] = exp.strftime(
                Cookies.EXPIRE_DATE_FORMAT)
            self.response.cookies.update(sck)
        elif session and SESSION_COOKIE_NAME in self.request.cookies:
            exp = datetime.datetime.utcfromtimestamp(0)
            sck = Cookies()
            sck[SESSION_COOKIE_NAME] = session.id
            sck[SESSION_COOKIE_NAME]["httponly"] = True
            sck[SESSION_COOKIE_NAME]["path"] = "/"
            sck[SESSION_COOKIE_NAME]["expires"] = exp.strftime(
                Cookies.EXPIRE_DATE_FORMAT)
            self.response.cookies.update(sck)

        if ctr_res is not None:
            if isinstance(ctr_res, tuple):
                status, headers, cks, body = self.__decode_tuple_response(
                    ctr_res)
                self.response.status_code = status if status is not None else self.response.status_code
                self.response.body = body if body is not None else self.response.body
                self.response.add_headers(headers)
                self.response.cookies.update(cks)
            elif isinstance(ctr_res, Response):
                self.response.status_code = ctr_res.status_code
                self.response.body = ctr_res.body
                self.response.add_headers(ctr_res.headers)
            elif isinstance(ctr_res, Redirect):
                self.response.send_redirect(ctr_res.url)
            elif isinstance(ctr_res, int) and ctr_res >= 200 and ctr_res < 600:
                self.response.status_code = ctr_res
            elif isinstance(ctr_res, Headers):
                self.response.add_headers(ctr_res)
            elif isinstance(ctr_res, cookies.BaseCookie):
                self.response.cookies.update(ctr_res)
            else:
                self.response.body = ctr_res

        if self.request.method.upper() == "HEAD":
            self.response.body = None
        if not self.response.is_sent:
            self.response.send_response()

    async def _do_request_async(self):
        ctr_res = await self._run_ctrl_fun()
        self._do_res(ctr_res)

    def do_chain(self):
        if self.response.is_sent:
            return
        if self.__filters:
            filter_func = self.__filters.pop(0)
            self.request._put_coroutine_task(
                self._wrap_to_async(filter_func, [self]))
        else:
            self.request._put_coroutine_task(self._do_request_async())

    async def _wrap_to_async(self, func: Callable, args: List = [], kwargs: Dict = {}) -> Any:
        if asyncio.iscoroutinefunction(func):
            return await func(*args, **kwargs)
        else:
            return func(*args, **kwargs)

    def __decode_tuple_response(self, ctr_res):
        status_code = None
        headers = Headers()
        cks = cookies.SimpleCookie()
        body = None
        for item in ctr_res:
            if isinstance(item, int):
                if status_code is None:
                    status_code = item
            elif isinstance(item, Headers):
                headers.update(item)
            elif isinstance(item, cookies.BaseCookie):
                cks.update(item)
            elif type(item) in (str, dict, StaticFile, bytes, bytearray):
                if body is None:
                    body = item
        return status_code, headers, cks, body

    async def __prepare_args(self):
        args = get_function_args(self.__controller.func)
        arg_vals = []
        if len(args) > 0:
            ctr_obj = self.__controller.ctrl_object
            if ctr_obj is not None:
                arg_vals.append(self.__controller.ctrl_object)
                args = args[1:]
        for arg, arg_type_anno in args:
            param = await self.__get_params_(arg, arg_type_anno)
            if param is None:
                raise HttpError(400, "Missing Paramter",
                                f"Parameter[{arg}] is required! ")
            arg_vals.append(param)

        return arg_vals

    async def __get_params_(self, arg, arg_type, val=None):
        if arg_type in self.__model_binding_conf.model_bingding_types:
            binding_type = self.__model_binding_conf.model_bingding_types[arg_type]
        else:
            binding_type = self.__model_binding_conf.default_model_binding_type

        binding_obj = binding_type(self.request,
                                   self.response,
                                   arg,
                                   arg_type,
                                   val)
        return await self._wrap_to_async(binding_obj.bind)

    async def __prepare_kwargs(self):
        kwargs = get_function_kwargs(self.__controller.func)
        if kwargs is None:
            return None
        kwarg_vals = {}
        for k, v, t in kwargs:
            kwarg_vals[k] = await self.__get_params_(
                k, type(v) if v is not None else t, v)

        return kwarg_vals


class HTTPControllerHandler:

    def __init__(self, http_request_handler, environment={}) -> None:
        self.method: str = http_request_handler.command
        self.request_path: str = http_request_handler.request_path
        self.query_string: str = http_request_handler.query_string
        self.query_parameters: Dict[str, List[str]
                                    ] = http_request_handler.query_parameters
        self.headers: Dict[str, str] = http_request_handler.headers

        self.routing_conf: RoutingServer = http_request_handler.routing_conf
        self.reader: StreamReader = http_request_handler.reader

        self.send_header = http_request_handler.send_header
        self.end_headers = http_request_handler.end_headers
        self.send_response = http_request_handler.send_response
        self.send_error = http_request_handler.send_error
        self.writer: StreamWriter = http_request_handler.writer
        self.environment: Dict[str, Any] = environment

    def __match_one_exp(self, d: Dict[str, Union[List[str], str]], exp: str, where: str) -> bool:
        if not exp:
            return True
        _logger.debug(
            f"Match controller {where} expression [{exp}] for values: {d}. ")
        exp_ = str(exp)
        e_idx = exp_.find("=")
        if e_idx < 0:
            _logger.debug(f"cannot find = in exp {exp}")
            if exp_.startswith('!'):
                return not exp_[1:] in d.keys()
            else:
                return exp_ in d.keys()
        idx = exp_.find("!=")
        if idx > 0 and idx < e_idx:
            k, v = break_into(exp_, '!=')
            if k not in d.keys():
                return False
            dvals = d[k]
            if isinstance(dvals, str):
                dvals = [dvals]
            for dv in dvals:
                if dv == v:
                    return False
            return True

        idx = exp_.find("^=")
        if idx > 0 and idx < e_idx:
            k, v = break_into(exp_, "^=")
            if k not in d.keys():
                return False
            dvals = d[k]
            if isinstance(dvals, str):
                dvals = [dvals]
            for dv in dvals:
                _logger.debug(f"^= mapping:: {dv} ... {v}")
                if dv.startswith(v):
                    return True
            return False

        if e_idx > 0:
            k, v = break_into(exp_, '=')
            if k not in d.keys():
                return False
            dvals = d[k]
            if isinstance(dvals, str):
                dvals = [dvals]
            for dv in dvals:
                if dv == v:
                    return True
            return False

        _logger.error(
            f"Controller {where} expression [{exp}] is not valied, returning False...")
        return False

    def __match_exps(self, d: Dict[str, Union[List[str], str]], exps: List[str], all: bool, where: str) -> bool:
        if not exps:
            return True

        for exp in exps:
            res = self.__match_one_exp(d, exp, where)
            if all and not res:
                return False
            if not all and res:  # match one
                return True
        # return if all True else False
        return all

    def __is_req_match_ctl(self, req: RequestWrapper, ctrl: _ControllerFunction) -> bool:
        _logger.debug(f"{ctrl.headers} - {ctrl.params}")
        return self.__match_exps(req.headers, ctrl.headers, ctrl.match_all_headers_expressions, 'headers') \
            and self.__match_exps(req.parameters, ctrl.params, ctrl.match_all_params_expressions, 'params')

    def __get_ctrl(self, req: RequestWrapper) -> Tuple[_ControllerFunction, Dict, List]:
        mth = self.method.upper()
        path = req._path
        ctrls = self.routing_conf.get_url_controllers(path, mth)
        for ctrl, pvs, regs in ctrls:
            if self.__is_req_match_ctl(req, ctrl):
                return ctrl, pvs, regs
        return None, {}, []

    async def handle_request(self):
        mth = self.method.upper()

        req = await self.__prepare_request(mth)

        ctrl, req.path_values, req.reg_groups = self.__get_ctrl(req)

        res = ResponseWrapper(self)
        if ctrl is None:
            res.send_error(404, "Controller Not Found",
                           "Cannot find a controller for your path")
        else:
            filters = self.routing_conf.get_matched_filters(req.path)
            ctx = FilterContextImpl(
                req, res, ctrl, self.routing_conf.model_binding_conf, filters)
            try:
                ctx.do_chain()
                if req._coroutine_objects:
                    _logger.debug(f"wait all the objects in waiting list.")
                    while req._coroutine_objects:
                        await req._coroutine_objects.pop(0)
            except HttpError as e:
                res.send_error(e.code, e.message, e.explain)
            except Exception as e:
                _logger.exception("error occurs! returning 500")
                res.send_error(500, None, str(e))

    async def __prepare_request(self, method) -> RequestWrapper:
        path = self.request_path
        req = RequestWrapper()
        req.environment = self.environment or {}
        req.path = "/" + path
        req._path = path
        req._session_fac = self.routing_conf.session_factory
        headers = {}
        _headers_keys_in_lowers: Dict[str, str] = {}
        for k in self.headers.keys():
            headers[k] = self.headers[k]
            _headers_keys_in_lowers[k.lower()] = self.headers[k]
        req.headers = headers
        req._headers_keys_in_lowcase = _headers_keys_in_lowers

        # cookies
        if "cookie" in _headers_keys_in_lowers.keys():
            req.cookies.load(_headers_keys_in_lowers["cookie"])

        req.method = method

        req.parameters = self.query_parameters

        if "content-length" in _headers_keys_in_lowers.keys():
            content_length = int(_headers_keys_in_lowers["content-length"])
            req.reader = RequestBodyReaderWrapper(self.reader, content_length)
        elif _headers_keys_in_lowers.get("transfer-encoding", "").lower() == "chunked":
            _logger.debug("chunked encoding")
            req.reader = RequestBodyReaderChunkedReader(self.reader)
        else:
            req.reader = RequestBodyReaderDumpWrapper(self.reader)

        content_type = _headers_keys_in_lowers.get("content-type", "")
        if content_type.lower().startswith("application/x-www-form-urlencoded"):
            charset = get_charset(content_type)
            req._body = await req.reader.read_to_end()
            body_decoded_params = decode_query_string(
                req._body.decode(charset))
        elif content_type.lower().startswith("multipart/form-data"):
            req._body = await req.reader.read_to_end()
            body_decoded_params = self.__decode_multipart(
                content_type, req._body.decode("ISO-8859-1"))
        elif content_type.lower().startswith("application/json"):
            charset = get_charset(content_type)
            req._body = await req.reader.read_to_end()
            req.json = json.loads(req._body.decode(charset))
            body_decoded_params = {}
        else:
            body_decoded_params = {}
        req.parameters = self.__merge(body_decoded_params, req.parameters)

        return req

    def __merge(self, dic0: Dict[str, List[str]], dic1: Dict[str, List[str]]):
        """Merge tow dictionaries of which the structure is {k:[v1, v2]}"""
        dic = dic0
        for k, v in dic1.items():
            if k not in dic.keys():
                dic[k] = v
            else:
                for i in v:
                    dic[k].append(i)
        return dic

    def __decode_multipart(self, content_type, data):
        boundary = "--" + content_type.split("; ")[1].split("=")[1]
        fields = data.split(boundary)
        # ignore the first empty row and the last end symbol
        fields = fields[1: len(fields) - 1]
        params = {}
        for field in fields:
            # trim the first and the last empty row
            f = field[field.index("\r\n") + 2: field.rindex("\r\n")]
            key, val = self.__decode_multipart_field(f)
            put_to(params, key, val)
        return params

    def __decode_multipart_field(self, field):
        # first line: Content-Disposition
        line, rest = self.__read_line(field)

        kvs = self.__decode_content_disposition(line)
        kname = kvs["name"].encode(
            "ISO-8859-1", errors="replace").decode(DEFAULT_ENCODING, errors="replace")
        if len(kvs) == 1:
            # this is a string field, the second line is an empty line, the rest is the value
            val = self.__read_line(rest)[1].encode(
                "ISO-8859-1", errors="replace").decode(DEFAULT_ENCODING, errors="replace")
        elif "filename" in kvs or "filename*" in kvs:
            if "filename*" in kvs:
                name_value = kvs["filename*"]
                idx = name_value.find("'")
                if idx <= 0:
                    encoding = DEFAULT_ENCODING
                else:
                    encoding = name_value[0:idx]
                name_value = name_value[idx + 1:]
                idx = name_value.find("'")
                filename = unquote(name_value[idx + 1:], encoding)
            else:
                filename = kvs["filename"].encode(
                    "ISO-8859-1", errors="replace").decode(DEFAULT_ENCODING, errors="replace")

            # the second line is Content-Type line
            ct_line, rest = self.__read_line(rest)
            content_type = ct_line.split(":")[1].strip()
            # the third line is an empty line, the rest is the value
            content = self.__read_line(rest)[1].encode(
                "ISO-8859-1", errors="replace")

            val = MultipartFile(kname, filename=filename,
                                content_type=content_type, content=content)
        else:
            val = "UNKNOWN"

        return kname, val

    def __decode_content_disposition(self, line) -> Dict[str, str]:
        cont_dis = {}
        es = line.split(";")[1:]
        for e in es:
            k, v = break_into(e.strip(), "=")
            if v.startswith('"') and v.endswith('"'):
                cont_dis[k] = v[1: -1]  # ignore the '"' symbol
            else:
                cont_dis[k] = v
        return cont_dis

    def __read_line(self, txt):
        return break_into(txt, "\r\n")

    def _send_response(self, response):
        try:
            headers = response["headers"]
            cks = response["cookies"]
            raw_body = response["body"]
            status_code = response["status_code"]
            content_type, body = decode_response_body(raw_body)

            self._send_res(status_code, headers, content_type, cks, body)

        except HttpError as e:
            self.send_error(e.code, e.message, e.explain)

    def __send_res_headers(self, status_code: int, headers: Dict[str, str] = {}, content_type: str = "", cks: Cookies = Cookies()):
        if "Content-Type" not in headers.keys() and "content-type" not in headers.keys():
            headers["Content-Type"] = content_type
        elif "content-type" in headers.keys():
            headers["Content-Type"] = headers["content-type"]
            del headers["content-type"]

        self.send_response(status_code)
        self.send_header("Last-Modified", str(date_time_string()))
        for k, v in headers.items():
            if isinstance(v, str):
                self.send_header(k, v)
            elif isinstance(v, list):
                for iov in v:
                    if isinstance(iov, str):
                        self.send_header(k, iov)

        for k in cks:
            ck = cks[k]
            self.send_header("Set-Cookie", ck.OutputString())

    def _send_and_end_res_headers(self, *args, **kwargs):
        """
        " For response object to send and writer headers.
        """
        self.__send_res_headers(*args, **kwargs)
        self.end_headers()

    def _should_send_gzip(self, headers: Dict[str, str]) -> bool:
        if "Accept-Encoding" not in self.headers.keys():
            return False
        accept_encoding = self.headers["Accept-Encoding"].split(",")
        acgzip = False
        for acoding in accept_encoding:
            if acoding.strip().lower().startswith("gzip"):
                acgzip = True
        if not acgzip:
            return False
        for ctype in self.routing_conf.gzip_content_types:
            if headers["Content-Type"].lower().startswith(ctype):
                return True
        return False

    def _send_res(self, status_code: int, headers: Dict[str, str] = {}, content_type: str = "", cks: Cookies = Cookies(), body: Union[str, bytes, bytearray, StaticFile] = None):
        self.__send_res_headers(status_code, headers, content_type, cks)
        if self._should_send_gzip(headers):
            self._send_gzip_data(body)
        else:
            self._send_raw_data(body)

    def _send_gzip_data(self, body):
        if body is None:
            self.send_header("Content-Length", 0)
            self.end_headers()
            return
        body_data = b''
        if isinstance(body, str):
            body_data = body.encode(DEFAULT_ENCODING, errors="replace")
        elif isinstance(body, bytes) or isinstance(body, bytearray):
            body_data = body
        elif isinstance(body, StaticFile):
            buffer_size = 1024 * 1024  # 1M
            with open(body.file_path, "rb") as in_file:
                buffer_data = in_file.read(buffer_size)
                while buffer_data:
                    body_data = body_data + buffer_data
                    buffer_data = in_file.read(buffer_size)

        gzip_data = gzip.compress(
            body_data, compresslevel=self.routing_conf.gzip_compress_level)

        self.send_header("Content-Encoding", "gzip")
        self.send_header("Content-Length", len(gzip_data))
        self.end_headers()
        self.writer.write(gzip_data)

    def _send_raw_data(self, body):
        if body is None:
            self.send_header("Content-Length", 0)
            self.end_headers()
        elif isinstance(body, str):
            data = body.encode(DEFAULT_ENCODING, errors="replace")
            self.send_header("Content-Length", len(data))
            self.end_headers()
            self.writer.write(data)
        elif isinstance(body, bytes) or isinstance(body, bytearray):
            self.send_header("Content-Length", len(body))
            self.end_headers()
            self.writer.write(body)
        elif isinstance(body, StaticFile):
            file_size = os.path.getsize(body.file_path)
            self.send_header("Content-Length", file_size)
            self.end_headers()
            buffer_size = 1024 * 1024  # 1M
            with open(body.file_path, "rb") as in_file:
                data = in_file.read(buffer_size)
                while data:
                    self.writer.write(data)
                    data = in_file.read(buffer_size)

# file: request_handlers/websocket_handler.py
# -*- coding: utf-8 -*-

import asyncio
import os
import struct
import errno

from threading import Lock
from base64 import b64encode
from hashlib import sha1
from typing import Dict, Tuple, Union, List
from uuid import uuid4
from socket import error as SocketError

# from ..utils.logger import get_logger
# from ..models import Headers, WebsocketCloseReason, WebsocketRequest, WebsocketSession
# from ..models import WEBSOCKET_OPCODE_BINARY, WEBSOCKET_OPCODE_CLOSE, WEBSOCKET_OPCODE_CONTINUATION, WEBSOCKET_OPCODE_PING, WEBSOCKET_OPCODE_PONG, WEBSOCKET_OPCODE_TEXT
# from ..models import DEFAULT_ENCODING
# _logger = get_logger("naja_atra.request_handlers.websocket_request_handler")

'''
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers

https://datatracker.ietf.org/doc/html/rfc6455

+-+-+-+-+-------+-+-------------+-------------------------------+
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+
'''

FIN = 0x80
OPCODE = 0x0f
MASKED = 0x80
PAYLOAD_LEN = 0x7f
PAYLOAD_LEN_EXT16 = 0x7e
PAYLOAD_LEN_EXT64 = 0x7f
GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
_BUFFER_SIZE = 1024 * 1024

OPTYPES = {
    WEBSOCKET_OPCODE_CONTINUATION: "CONTINUATION",
    WEBSOCKET_OPCODE_TEXT: "TEXT",
    WEBSOCKET_OPCODE_BINARY: "BINARY",
    WEBSOCKET_OPCODE_CLOSE: "CLOSE",
    WEBSOCKET_OPCODE_PING: "PING",
    WEBSOCKET_OPCODE_PONG: "PONE",
}


class _ContinuationMessageCache:

    def __init__(self, opcode: int) -> None:
        self.opcode: int = opcode
        self.message_bytes: bytearray = bytearray()


class WebsocketException(Exception):

    def __init__(self, reason: WebsocketCloseReason = None, graceful: bool = False) -> None:
        super().__init__(reason)
        self.__graceful: bool = graceful
        self.__reason: WebsocketCloseReason = reason

    @property
    def is_graceful(self) -> bool:
        return self.__graceful

    @property
    def reason(self) -> WebsocketCloseReason:
        return self.__reason


class WebsocketControllerHandler:

    def __init__(self, http_request_handler) -> None:
        self.http_request_handler = http_request_handler
        self.request_writer = http_request_handler.request_writer
        self.routing_conf = http_request_handler.routing_conf
        self.send_response = http_request_handler.send_response_only
        self.send_header = http_request_handler.send_header
        self.reader = http_request_handler.reader
        self.keep_alive = True
        self.handshake_done = False

        handler_class, path_values, regroups = self.routing_conf.get_websocket_handler(
            http_request_handler.request_path)
        self.handler = handler_class.ctrl_object if handler_class else None
        self.ws_request = WebsocketRequest()
        self.ws_request.headers = http_request_handler.headers
        self.ws_request.path = http_request_handler.request_path
        self.ws_request.query_string = http_request_handler.query_string
        self.ws_request.parameters = http_request_handler.query_parameters
        self.ws_request.path_values = path_values
        self.ws_request.reg_groups = regroups
        if "cookie" in self.ws_request.headers:
            self.ws_request.cookies.load(self.ws_request.headers["cookie"])
        elif "Cookie" in self.ws_request.headers:
            self.ws_request.cookies.load(self.ws_request.headers["Cookie"])
        self.session = WebsocketSessionImpl(self, self.ws_request)
        self.close_reason: WebsocketCloseReason = None

        self._continution_cache: _ContinuationMessageCache = None
        self._send_msg_lock = Lock()
        self._send_frame_lock = Lock()

    @property
    def response_headers(self):
        if hasattr(self.http_request_handler, "_headers_buffer"):
            return self.http_request_handler._headers_buffer
        else:
            return []

    async def await_func(self, obj):
        if asyncio.iscoroutine(obj):
            return await obj
        return obj

    async def on_handshake(self) -> Tuple[int, Dict[str, List[str]]]:
        try:
            if not hasattr(self.handler, "on_handshake") or not callable(self.handler.on_handshake):
                return None, {}
            res = await self.await_func(self.handler.on_handshake(self.ws_request))
            http_status_code = None
            headers = {}
            if not res:
                pass
            elif isinstance(res, int):
                http_status_code = res
            elif isinstance(res, dict) or isinstance(res, Headers):
                headers = res
            elif isinstance(res, tuple):
                for item in res:
                    if isinstance(item, int) and not http_status_code:
                        http_status_code = item
                    elif isinstance(item, dict) or isinstance(item, Headers):
                        headers.update(item)
            else:
                _logger.warn(f"Endpoint[{self.ws_request.path}]")
            return http_status_code, headers
        except Exception as e:
            _logger.error(f"Error occurs when handshake. ")
            return 500, {}

    async def on_message(self, opcode: int, message_bytes: bytearray):
        try:
            if opcode == WEBSOCKET_OPCODE_CLOSE:
                _logger.info("Client asked to close connection.")
                if len(message_bytes) >= 2:
                    code = struct.unpack(">H", message_bytes[0:2])[0]
                    reason = message_bytes[2:].decode(
                        'UTF-8', errors="replace")
                else:
                    code = None
                    reason = ''
                raise WebsocketException(graceful=True, reason=WebsocketCloseReason(
                    "Client asked to close connection.", code=code, reason=reason))
            elif opcode == WEBSOCKET_OPCODE_TEXT and hasattr(self.handler, "on_text_message") and callable(self.handler.on_text_message):
                await self.await_func(self.handler.on_text_message(self.session, message_bytes.decode("UTF-8", errors="replace")))
            elif opcode == WEBSOCKET_OPCODE_PING and hasattr(self.handler, "on_ping_message") and callable(self.handler.on_ping_message):
                await self.await_func(self.handler.on_ping_message(self.session, bytes(message_bytes)))
            elif opcode == WEBSOCKET_OPCODE_PONG and hasattr(self.handler, "on_pong_message") and callable(self.handler.on_pong_message):
                await self.await_func(self.handler.on_pong_message(self.session, bytes(message_bytes)))
            elif opcode == WEBSOCKET_OPCODE_BINARY and self._continution_cache.message_bytes and hasattr(self.handler, "on_binary_message") and callable(self.handler.on_binary_message):
                await self.await_func(self.handler.on_binary_message(self.session, bytes(message_bytes)))
        except Exception as e:
            _logger.error(f"Error occurs when on message!")
            self.close(f"Error occurs when on_message. {e}")

    async def on_continuation_frame(self, first_frame_opcode: int, fin: int, message_frame: bytearray):
        try:
            if first_frame_opcode == WEBSOCKET_OPCODE_BINARY and hasattr(self.handler, "on_binary_frame") and callable(self.handler.on_binary_frame):
                should_append_to_cache = await self.await_func(self.handler.on_binary_frame(self.session, bool(fin), bytes(message_frame)))
                if should_append_to_cache == True:
                    self._continution_cache.message_bytes.extend(message_frame)
            else:
                self._continution_cache.message_bytes.extend(message_frame)
        except Exception as e:
            _logger.error(f"Error occurs when on message!")
            self.close(f"Error occurs when on_message. {e}")

    async def on_open(self):
        try:
            if hasattr(self.handler, "on_open") and callable(self.handler.on_open):
                await self.await_func(self.handler.on_open(self.session))
        except Exception as e:
            _logger.error(f"Error occurs when on open!")
            self.close(f"Error occurs when on_open. {e}")

    async def on_close(self):
        try:
            if hasattr(self.handler, "on_close") and callable(self.handler.on_close):
                await self.await_func(self.handler.on_close(self.session, self.close_reason))
        except Exception as e:
            _logger.error(f"Error occurs when on close!")

    async def handle_request(self):
        while self.keep_alive:
            try:
                if not self.handshake_done:
                    await self.handshake()
                else:
                    await self.read_next_message()
            except WebsocketException as e:
                if not e.is_graceful:
                    _logger.warning(
                        f"Something's wrong, close connection: {e.reason}")
                else:
                    _logger.info(f"Close connection: {e.reason}")
                self.keep_alive = False
                self.close_reason = e.reason
            except:
                _logger.exception("Errors occur when handling message!")
                self.keep_alive = False
                self.close_reason = WebsocketCloseReason(
                    "Errors occur when handling message!")

        await self.on_close()

    async def handshake(self):
        if self.handler:
            code, headers = await self.on_handshake()
            if code and code != 101:
                self.keep_alive = False
                self.send_response(code)
            else:
                self.send_response(101, "Switching Protocols")
                self.send_header("Upgrade", "websocket")
                self.send_header("Connection", "Upgrade")
                self.send_header("Sec-WebSocket-Accept",
                                 self.calculate_response_key())
            if headers:
                for h_name, h_val in headers.items():
                    self.send_header(h_name, h_val)
        else:
            self.keep_alive = False
            self.send_response(404)

        ws_res_headers = b"".join(self.response_headers) + b"\r\n"
        _logger.debug(ws_res_headers)
        self.request_writer.send(ws_res_headers)
        self.handshake_done = True
        if self.keep_alive == True:
            await self.on_open()

    def calculate_response_key(self):
        key: str = self.ws_request.headers["Sec-WebSocket-Key"] if "Sec-WebSocket-Key" in self.ws_request.headers else self.ws_request.headers["Sec-Websocket-Key"]
        _logger.debug(
            f"Sec-WebSocket-Key: {key}")
        key_hash = sha1(key.encode(errors="replace") +
                        GUID.encode(errors="replace"))
        response_key = b64encode(key_hash.digest()).strip()
        return response_key.decode('ASCII', errors="replace")

    async def read_bytes(self, num):
        return await self.reader.read(num)

    async def _read_message_content(self) -> Tuple[int, int, bytearray]:
        _logger.debug(f"Read next websocket[{self.ws_request.path}] message")
        try:
            b1, b2 = await self.read_bytes(2)
        except ConnectionResetError as e:
            raise WebsocketException(
                graceful=True, reason=WebsocketCloseReason("Client closed connection."))
        except SocketError as e:
            if e.errno == errno.ECONNRESET:
                raise WebsocketException(
                    graceful=True, reason=WebsocketCloseReason("Client closed connection."))
            b1, b2 = 0, 0
        except ValueError as e:
            b1, b2 = 0, 0

        fin = b1 & FIN
        opcode = b1 & OPCODE
        masked = b2 & MASKED
        payload_length = b2 & PAYLOAD_LEN

        if not masked:
            raise WebsocketException(
                reason=WebsocketCloseReason("Client is not masked."))

        if opcode not in OPTYPES.keys():
            raise WebsocketException(
                reason=WebsocketCloseReason(f"Unknown opcode {opcode}."))

        if opcode in (WEBSOCKET_OPCODE_PING, WEBSOCKET_OPCODE_PONG) and payload_length > 125:
            raise WebsocketException(reason=WebsocketCloseReason(
                f"Ping/Pong message payload is too large! The max length of the Ping/Pong messages is 125. but now is {payload_length}"))

        if payload_length == 126:
            hb = await self.reader.read(2)
            payload_length = struct.unpack(">H", hb)[0]
        elif payload_length == 127:
            qb = await self.reader.read(8)
            payload_length = struct.unpack(">Q", qb)[0]

        frame_bytes = bytearray()
        if payload_length > 0:
            masks = await self.read_bytes(4)
            payload = await self.read_bytes(payload_length)
            for encoded_byte in payload:
                frame_bytes.append(encoded_byte ^ masks[len(frame_bytes) % 4])

        return fin, opcode, frame_bytes

    async def read_next_message(self):

        fin, opcode, frame_bytes = await self._read_message_content()

        if fin and opcode != WEBSOCKET_OPCODE_CONTINUATION:
            # A normal frame, handle message.
            await self.on_message(opcode, frame_bytes)
            return

        if not fin and opcode != WEBSOCKET_OPCODE_CONTINUATION:
            # Fragment message: first frame, try to create a cache object.
            if opcode not in (WEBSOCKET_OPCODE_TEXT, WEBSOCKET_OPCODE_BINARY):
                raise WebsocketException(reason=WebsocketCloseReason(
                    f"Control({OPTYPES[opcode]}) frames MUST NOT be fragmented"))

            if self._continution_cache is not None:
                # Check if another fragment message is being read.
                raise WebsocketException(reason=WebsocketCloseReason(
                    "Another continution message is not yet finished. Close connection for this error!"))

            self._continution_cache = _ContinuationMessageCache(opcode)

        if self._continution_cache is None:
            # When the first frame is not send, close connection.
            raise WebsocketException(reason=WebsocketCloseReason(
                "A continuation fragment frame is received, but the start fragment is not yet received. "))

        await self.on_continuation_frame(self._continution_cache.opcode, fin, frame_bytes)

        if fin:
            # Fragment message: end of this message.
            await self.on_message(self._continution_cache.opcode, self._continution_cache.message_bytes)
            self._continution_cache = None

    def send_message(self, message: Union[bytes, str], chunk_size: int = 0):
        if isinstance(message, bytes):
            self.send_bytes(WEBSOCKET_OPCODE_TEXT,
                            message, chunk_size=chunk_size)
        elif isinstance(message, str):
            self.send_bytes(WEBSOCKET_OPCODE_TEXT, message.encode(
                DEFAULT_ENCODING, errors="replace"), chunk_size=chunk_size)
        else:
            _logger.error(f"Cannot send message[{message}. ")

    def send_ping(self, message: Union[str, bytes]):
        if isinstance(message, bytes):
            self.send_bytes(WEBSOCKET_OPCODE_PING, message)
        elif isinstance(message, str):
            self.send_bytes(WEBSOCKET_OPCODE_PING, message.encode(
                DEFAULT_ENCODING, errors="replace"))

    def send_pong(self, message: Union[str, bytes]):
        if isinstance(message, bytes):
            self.send_bytes(WEBSOCKET_OPCODE_PONG, message)
        elif isinstance(message, str):
            self.send_bytes(WEBSOCKET_OPCODE_PONG, message.encode(
                DEFAULT_ENCODING, errors="replace"))

    def send_bytes(self, opcode: int, payload: bytes, chunk_size: int = 0):
        if opcode not in OPTYPES.keys() or opcode == WEBSOCKET_OPCODE_CONTINUATION:
            raise WebsocketException(reason=WebsocketCloseReason(
                f"Cannot send message in a opcode {opcode}. "))

        # Control frames MUST NOT be fragmented.
        c_size = chunk_size if opcode in (
            WEBSOCKET_OPCODE_BINARY, WEBSOCKET_OPCODE_TEXT) else 0

        if c_size and c_size > 0:
            with self._send_msg_lock:
                # Make sure a fragmented message is sent completely.
                self._send_bytes_no_lock(opcode, payload, chunk_size=c_size)
        else:
            self._send_bytes_no_lock(opcode, payload)

    def _send_bytes_no_lock(self, opcode: int, payload: bytes, chunk_size: int = 0):
        frame_size = chunk_size if chunk_size and chunk_size > 0 else None
        all_payloads = payload
        frame_bytes = b''
        while all_payloads:
            op = WEBSOCKET_OPCODE_CONTINUATION if frame_bytes else opcode
            frame_bytes = all_payloads[0: frame_size]
            all_payloads = all_payloads[frame_size:] if frame_size else b''
            fin = 0 if all_payloads else FIN
            self._send_frame(fin, op, frame_bytes)

    def _send_frame(self, fin: int, opcode: int, payload: bytes):
        with self._send_frame_lock:
            self.request_writer.send(
                self._create_frame_header(fin, opcode, len(payload)))
            self.request_writer.send(payload)

    def _create_frame_header(self, fin: int, opcode: int, payload_length: int) -> bytes:
        header = bytearray()
        # Normal payload
        if payload_length <= 125:
            header.append(fin | opcode)
            header.append(payload_length)

        # Extended payload
        elif payload_length >= 126 and payload_length <= 65535:
            header.append(fin | opcode)
            header.append(PAYLOAD_LEN_EXT16)
            header.extend(struct.pack(">H", payload_length))

        # Huge extended payload
        elif payload_length < 18446744073709551616:
            header.append(fin | opcode)
            header.append(PAYLOAD_LEN_EXT64)
            header.extend(struct.pack(">Q", payload_length))
        else:
            raise Exception(
                "Message is too big. Consider breaking it into chunks.")

        return header

    def send_file(self, path: str, chunk_size: int = 0):
        try:
            file_size = os.path.getsize(path)
            if not chunk_size or chunk_size < 0 or chunk_size > file_size:
                self._send_file_no_lock(path, file_size, file_size)
            else:
                with self._send_msg_lock:
                    self._send_file_no_lock(path, file_size, chunk_size)
        except (OSError, ValueError):
            raise WebsocketException(reason=WebsocketCloseReason(
                f"File in {path} does not exist or is not accessible."))

    def _send_file_no_lock(self, path: str, file_size: int, chunk_size: int):
        with open(path, 'rb') as in_file:
            remain_bytes = file_size
            opcode = WEBSOCKET_OPCODE_BINARY
            while remain_bytes > 0:
                with self._send_frame_lock:
                    frame_size = min(remain_bytes, chunk_size)
                    remain_bytes -= frame_size

                    fin = 0 if remain_bytes > 0 else FIN

                    self.request_writer.send(
                        self._create_frame_header(fin, opcode, frame_size))
                    while frame_size > 0:
                        buff_size = min(_BUFFER_SIZE, frame_size)
                        frame_size -= buff_size

                        data = in_file.read(buff_size)
                        self.request_writer.send(data)
                # After the first frame, the opcode of other frames is continuation forever.
                opcode = WEBSOCKET_OPCODE_CONTINUATION

    def close(self, reason: str = ""):
        self.send_bytes(WEBSOCKET_OPCODE_CLOSE, reason.encode(
            DEFAULT_ENCODING, errors="replace"))
        self.keep_alive = False
        self.close_reason = WebsocketCloseReason(
            "Server asked to close connection.")


class WebsocketSessionImpl(WebsocketSession):

    def __init__(self, handler: WebsocketControllerHandler, request: WebsocketRequest) -> None:
        self.__id = uuid4().hex
        self.__handler = handler
        self.__request = request

    @ property
    def id(self) -> str:
        return self.__id

    @ property
    def request(self) -> WebsocketRequest:
        return self.__request

    @ property
    def is_closed(self) -> bool:
        return not self.__handler.keep_alive

    def send_ping(self, message: bytes = b''):
        self.__handler.send_ping(message)

    def send_pone(self, message: bytes = b''):
        self.__handler.send_pong(message)

    def send(self, message: Union[str, bytes], opcode: int = WEBSOCKET_OPCODE_TEXT, chunk_size: int = 0):
        if isinstance(message, bytes):
            msg = message
        elif isinstance(message, str):
            msg = message.encode(DEFAULT_ENCODING, errors="replace")
        else:
            raise WebsocketException(reason=WebsocketCloseReason(
                f"message {message} is not a string nor a bytes object, cannot send it to client. "))
        self.__handler.send_bytes(
            opcode if opcode is None else WEBSOCKET_OPCODE_TEXT, msg, chunk_size=chunk_size)

    def send_text(self, message: str, chunk_size: int = 0):
        self.__handler.send_message(message, chunk_size=chunk_size)

    def send_binary(self, binary: bytes, chunk_size: int = 0):
        self.__handler.send_bytes(
            WEBSOCKET_OPCODE_BINARY, binary, chunk_size=chunk_size)

    def send_file(self, path: str, chunk_size: int = 0):
        self.__handler.send_file(path, chunk_size=chunk_size)

    def close(self, reason: str = ""):
        self.__handler.close(reason)

# file: request_handlers/http_request_handler.py
# -*- coding: utf-8 -*-
import html
import re
import http.client
import email.parser
import email.message
import socketserver
import asyncio
import socket


from typing import Any, Dict
from http import HTTPStatus
from urllib.parse import unquote
from asyncio.streams import StreamReader, StreamWriter
from http import HTTPStatus

# from .. import name, version
# from ..models import RequestBodyReader
# from ..utils import http_utils
# from ..http_servers.routing_server import RoutingServer
# from .http_controller_handler import HTTPControllerHandler
# from .websocket_controller_handler import WebsocketControllerHandler

_LINE_MAX_BYTES = 65536
_MAXHEADERS = 100


class RequestWriter:

    def __init__(self, writer: StreamWriter) -> None:
        self.writer: StreamWriter = writer

    def send(self, data: bytes):
        self.writer.write(data)


class HttpRequestHandler:

    server_version = f"{name}/{version}"

    default_request_version = "HTTP/1.1"

    # The version of the HTTP protocol we support.
    # Set this to HTTP/1.1 to enable automatic keepalive
    protocol_version = "HTTP/1.1"

    # MessageClass used to parse headers
    _message_class = http.client.HTTPMessage

    # hack to maintain backwards compatibility
    responses = {
        v: (v.phrase, v.description)
        for v in HTTPStatus.__members__.values()
    }

    def __init__(self, reader: StreamReader, writer: StreamWriter, request_writer=None, routing_conf: RoutingServer = None) -> None:
        self.routing_conf: RoutingServer = routing_conf
        self.reader: StreamReader = reader
        self.writer: StreamWriter = writer
        self.request_writer: RequestWriter = request_writer if request_writer else RequestWriter(
            writer)

        self.requestline = ''
        self.request_version = ''
        self.command = ''
        self.path = ''
        self.request_path = ''
        self.query_string = ''
        self.query_parameters = {}
        self.headers = {}

        self.close_connection = True
        self._keep_alive = self.routing_conf.keep_alive
        self._connection_idle_time = routing_conf.connection_idle_time
        self._keep_alive_max_req = routing_conf.keep_alive_max_request
        self.req_count = 0

    async def parse_request(self):
        self.req_count += 1
        try:
            if hasattr(self.reader, "connection"):
                # For blocking io. asyncio.wait_for will not raise TimeoutError if the io is blocked.
                self.reader.connection.settimeout(self._connection_idle_time)
            raw_requestline = await asyncio.wait_for(self.reader.readline(), self._connection_idle_time)
            if hasattr(self.reader, "connection") and hasattr(self.reader, "timeout"):
                # For blocking io. Set the Original timeout to the connection.
                self.reader.connection.settimeout(self.reader.timeout)
        except asyncio.TimeoutError:
            _logger.warn("Wait for reading request line timeout. ")
            return False
        if len(raw_requestline) > _LINE_MAX_BYTES:
            self.requestline = ''
            self.request_version = ''
            self.command = ''
            self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG)
            return False
        if not raw_requestline:
            self.close_connection = True
            return False
        self.command = None
        self.request_version = version = self.default_request_version
        self.close_connection = True
        requestline = str(raw_requestline, 'iso-8859-1')
        requestline = requestline.rstrip('\r\n')
        self.requestline = requestline
        words = requestline.split()
        if len(words) == 0:
            return False

        if len(words) >= 3:  # Enough to determine protocol version
            version = words[-1]
            try:
                if not version.startswith('HTTP/'):
                    raise ValueError
                base_version_number = version.split('/', 1)[1]
                version_number = base_version_number.split(".")
                # RFC 2145 section 3.1 says there can be only one "." and
                #   - major and minor numbers MUST be treated as
                #      separate integers;
                #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
                #      turn is lower than HTTP/12.3;
                #   - Leading zeros MUST be ignored by recipients.
                if len(version_number) != 2:
                    raise ValueError
                version_number = int(version_number[0]), int(version_number[1])
            except (ValueError, IndexError):
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    f"Bad request version {version}")
                return False

            if version_number >= (2, 0):
                self.send_error(
                    HTTPStatus.HTTP_VERSION_NOT_SUPPORTED,
                    f"Invalid HTTP version {base_version_number}")
                return False
            self.request_version = version
            _logger.debug(f"request version: {self.request_version}")
        if not 2 <= len(words) <= 3:
            self.send_error(
                HTTPStatus.BAD_REQUEST,
                "Bad request syntax (%r)" % requestline)
            return False
        command, path = words[:2]
        if len(words) == 2:
            self.close_connection = True
            if command != 'GET':
                self.send_error(
                    HTTPStatus.BAD_REQUEST,
                    "Bad HTTP/0.9 request type (%r)" % command)
                return False
        self.command, self.path = command, path

        self.request_path = self._get_request_path(self.path)

        self.query_string = self.__get_query_string(self.path)

        self.query_parameters = decode_query_string(
            self.query_string)

        # Examine the headers and look for a Connection directive.
        try:
            self.headers = await self.parse_headers()
        except http.client.LineTooLong as err:
            self.send_error(
                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
                "Line too long",
                str(err))
            return False
        except http.client.HTTPException as err:
            self.send_error(
                HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE,
                "Too many headers",
                str(err)
            )
            return False

        conntype = self.headers.get('Connection', '')

        self.close_connection = not self._keep_alive or conntype.lower(
        ) != 'keep-alive' or self.protocol_version != "HTTP/1.1"

        # Examine the headers and look for an Expect directive
        expect = self.headers.get('Expect', "")
        if (expect.lower() == "100-continue" and
                self.protocol_version >= "HTTP/1.1" and
                self.request_version >= "HTTP/1.1"):
            if not self.handle_expect_100():
                return False
        return True

    async def parse_headers(self):
        """Parses only RFC2822 headers from a file pointer.

        email Parser wants to see strings rather than bytes.
        But a TextIOWrapper around self.rfile would buffer too many bytes
        from the stream, bytes which we later need to read as bytes.
        So we read the correct bytes here, as bytes, for email Parser
        to parse.

        """
        headers = []
        while True:
            line = await self.reader.readline()
            if len(line) > _LINE_MAX_BYTES:
                raise http.client.LineTooLong("header line")
            headers.append(line)
            if len(headers) > _MAXHEADERS:
                raise http.client.HTTPException(
                    f"got more than {_MAXHEADERS} headers")
            if line in (b'\r\n', b'\n', b''):
                break
        hstring = b''.join(headers).decode('iso-8859-1')

        return email.parser.Parser(_class=self._message_class).parsestr(hstring)

    def handle_expect_100(self):
        """Decide what to do with an "Expect: 100-continue" header.

        If the client is expecting a 100 Continue response, we must
        respond with either a 100 Continue or a final response before
        waiting for the request body. The default is to always respond
        with a 100 Continue. You can behave differently (for example,
        reject unauthorized requests) by overriding this method.

        This method should either return True (possibly after sending
        a 100 Continue response) or send an error response and return
        False.

        """
        self.send_response_only(HTTPStatus.CONTINUE)
        self.end_headers()
        return True

    def __get_query_string(self, ori_path: str):
        parts = ori_path.split('?')
        if len(parts) == 2:
            return parts[1]
        else:
            return ""

    def _get_request_path(self, ori_path: str):
        path = ori_path.split('?', 1)[0]
        path = path.split('#', 1)[0]
        path = remove_url_first_slash(path)
        path = unquote(path)
        return path

    def send_error(self, code: int, message: str = None, explain: str = None, headers: Dict[str, str] = {}):
        try:
            shortmsg, longmsg = self.responses[code]
        except KeyError:
            shortmsg, longmsg = '???', '???'
        if message is None:
            message = shortmsg
        if explain is None:
            explain = longmsg
        self.log_error(f"code {code}, message {message}")
        self.send_response(code, message)
        self.send_header('Connection', 'close')

        # Message body is omitted for cases described in:
        #  - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified)
        #  - RFC7231: 6.3.6. 205(Reset Content)
        body = None
        if (code >= 200 and
            code not in (HTTPStatus.NO_CONTENT,
                         HTTPStatus.RESET_CONTENT,
                         HTTPStatus.NOT_MODIFIED)):
            try:
                content: Any = self.routing_conf.error_page(code, html.escape(
                    message, quote=False), html.escape(explain, quote=False))
            except:
                content: str = html.escape(
                    message, quote=False) + ":" + html.escape(explain, quote=False)
            content_type, body = decode_response_body_to_bytes(
                content)

            self.send_header("Content-Type", content_type)
            self.send_header('Content-Length', str(len(body)))
        if headers:
            for h_name, h_val in headers.items():
                self.send_header(h_name, h_val)
        self.end_headers()

        if self.command != 'HEAD' and body:
            self.writer.write(body)

    def send_response(self, code, message=None):
        """Add the response header to the headers buffer and log the
        response code.

        Also send two standard headers with the server software
        version and the current date.

        """
        self.log_request(code)
        self.send_response_only(code, message)
        self.send_header('Server', self.server_version)
        self.send_header('Date', date_time_string())

    def send_header(self, keyword: str, value: str):
        """Send a MIME header to the headers buffer."""
        if keyword.lower() == 'connection':
            if value.lower() == 'close':
                self.close_connection = True
            elif value.lower() == 'keep-alive':
                if self._keep_alive:
                    self.close_connection = False
                else:
                    _logger.warning(
                        f"Keep Alive configuration is set to False, won't send keep-alive header.")
                    return

        if self.request_version != 'HTTP/0.9':
            if not hasattr(self, '_headers_buffer'):
                self._headers_buffer = []
            self._headers_buffer.append(
                f"{keyword}: {value}\r\n".encode('latin-1', errors='strict'))

    def end_headers(self):
        """Send the blank line ending the MIME headers."""
        if self.request_version != 'HTTP/0.9':
            self._headers_buffer.append(b"\r\n")
            self.flush_headers()

    def flush_headers(self):
        if hasattr(self, '_headers_buffer'):
            self.writer.write(b"".join(self._headers_buffer))
            self._headers_buffer = []

    def send_response_only(self, code, message: str = None):
        """Send the response header only."""
        if self.request_version != 'HTTP/0.9':
            if message is None:
                if code in self.responses:
                    message = self.responses[code][0]
                else:
                    message = ''
            if not hasattr(self, '_headers_buffer'):
                self._headers_buffer = []
            self._headers_buffer \
                .append(f"{self.protocol_version} {code} {message}\r\n".encode('latin-1', errors='strict'))

    def log_request(self, code='-', size='-'):
        if isinstance(code, HTTPStatus):
            code = code.value
        self.log_message('"%s" %s %s',
                         self.requestline, str(code), str(size))

    def log_error(self, format, *args):
        self.log_message(format, *args)

    def log_message(self, format, *args):
        _logger.info(f"{format % args}")

    def set_prefer_keep_alive_params(self):
        pass

    def set_alive_params(self):
        if "Keep-Alive" in self.headers:
            ka_header = self.headers["Keep-Alive"]
            timeout_match = re.match(r"^.*timeout=(\d+).*$", ka_header)
            if timeout_match:
                self._connection_idle_time = int(timeout_match.group(1))
            max_match = re.match(r"^.*max=(\d+).*$", ka_header)
            if max_match:
                self._keep_alive_max_req = int(max_match.group(1))

    async def handle_request(self):
        parse_request_success = await self.parse_request()
        if not parse_request_success:
            return
        self.set_alive_params()

        if self.request_version == "HTTP/1.1" and self.command == "GET" and "Upgrade" in self.headers and self.headers["Upgrade"] == "websocket":
            _logger.debug("This is a websocket connection. ")
            ws_handler = WebsocketControllerHandler(self)
            await ws_handler.handle_request()
            self.writer.write_eof()
            return

        await self.handle_http_request()
        while not self.close_connection:
            _logger.debug("Keep-Alive, read next request. ")
            parse_request_success = await self.parse_request()
            if not parse_request_success:
                _logger.debug("parse request fails, return. ")
                return
            if self.req_count >= self._keep_alive_max_req:
                self.send_response("Connection", "close")
            await self.handle_http_request()
            _logger.debug("Handle a keep-alive request successfully!")

    async def handle_http_request(self):
        try:
            http_handler = HTTPControllerHandler(self)
            await http_handler.handle_request()
            if self.writer.can_write_eof():
                self.writer.write_eof()
        except socket.timeout as e:
            # a read or a write timed out.  Discard this connection
            self.log_error("Request timed out: %r", e)
            self.close_connection = True
            return


class SocketServerStreamRequestHandlerWraper(socketserver.StreamRequestHandler):

    server_version = HttpRequestHandler.server_version

    # Wrapper method for readline
    async def readline(self):
        return self.rfile.readline(_LINE_MAX_BYTES)

    async def read(self, n: int = -1):
        return self.rfile.read(n)

    def write(self, data: bytes):
        self.wfile.write(data)

    def can_write_eof(self) -> bool:
        return True

    def write_eof(self):
        self.wfile.flush()

    def close(self):
        self.wfile.close()

    def handle(self) -> None:
        handler: HttpRequestHandler = HttpRequestHandler(
            self, self, request_writer=self.request, routing_conf=self.server)
        asyncio.run(handler.handle_request())

    def finish(self) -> None:
        _logger.debug("Finish a socket connection.")
        return super().finish()

# file: http_servers/coroutine_http_server.py
# -*- coding: utf-8 -*-

# from .routing_server import RoutingServer
# from ..request_handlers.model_bindings import ModelBindingConf
# from ..request_handlers.http_request_handler import HttpRequestHandler

import asyncio
import threading
from asyncio.base_events import Server
from asyncio.streams import StreamReader, StreamWriter
from ssl import SSLContext
from time import sleep

# _logger = get_logger("naja_atra.http_servers.coroutine_http_server")


class CoroutineHTTPServer(RoutingServer):

    def __init__(self, host: str = '', port: int = 9090, ssl: SSLContext = None, res_conf={}, model_binding_conf: ModelBindingConf = ModelBindingConf()) -> None:
        RoutingServer.__init__(
            self, res_conf, model_binding_conf=model_binding_conf)
        self.host: str = host
        self.port: int = port
        self.ssl: SSLContext = ssl
        self.server: Server = None
        self.__thread_local = threading.local()

    async def callback(self, reader: StreamReader, writer: StreamWriter):
        handler = HttpRequestHandler(reader, writer, routing_conf=self)
        await handler.handle_request()
        _logger.debug("Connection ends, close the writer.")
        writer.close()

    async def start_async(self):
        self.server = await asyncio.start_server(
            self.callback, host=self.host, port=self.port, ssl=self.ssl)
        async with self.server:
            try:
                await self.server.serve_forever()
            except asyncio.CancelledError:
                _logger.debug(
                    "Some requests are lost for the reason that the server is shutted down.")
            finally:
                await self.server.wait_closed()

    def _get_event_loop(self) -> asyncio.AbstractEventLoop:
        if not hasattr(self.__thread_local, "event_loop"):
            try:
                self.__thread_local.event_loop = asyncio.new_event_loop()
            except:
                self.__thread_local.event_loop = asyncio.get_event_loop()
        return self.__thread_local.event_loop

    def start(self):
        self._get_event_loop().run_until_complete(self.start_async())

    def _shutdown(self):
        _logger.info("Try to shutdown server.")
        self.server.close()
        loop = self.server.get_loop()
        loop.call_soon_threadsafe(loop.stop)

    def shutdown(self, force=False):
        if force:
            return self._shutdown()
        wait_time = 3
        while wait_time:
            sleep(1)
            _logger.info(f"couting to shutdown: {wait_time}")
            wait_time = wait_time - 1
            if wait_time == 0:
                _logger.info("shutdown server....")
                self._shutdown()

# file: http_servers/thread_http_server.py
# -*- coding: utf-8 -*-

# from .routing_server import RoutingServer
# from ..request_handlers.model_bindings import ModelBindingConf
# from ..request_handlers.http_request_handler import SocketServerStreamRequestHandlerWraper
# from ..utils.logger import get_logger

import socket
import threading
from concurrent.futures import ThreadPoolExecutor
from socketserver import TCPServer

# _logger = get_logger("naja_atra.http_servers.threading_http_server")


class ThreadingHTTPServer(TCPServer, RoutingServer):

    allow_reuse_address = 1    # Seems to make sense in testing environment

    _default_max_workers = 50

    def server_bind(self):
        """Override server_bind to store the server name."""
        TCPServer.server_bind(self)
        host, port = self.server_address[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

    def __init__(self, addr, res_conf={},  model_binding_conf: ModelBindingConf = ModelBindingConf(), max_workers: int = None):
        RoutingServer.__init__(
            self, res_conf, model_binding_conf=model_binding_conf)
        self.max_workers = max_workers or self._default_max_workers
        self.threadpool: ThreadPoolExecutor = ThreadPoolExecutor(
            thread_name_prefix="ReqThread",
            max_workers=self.max_workers)
        TCPServer.__init__(self, addr, SocketServerStreamRequestHandlerWraper)

    def process_request_thread(self, request, client_address):
        try:
            self.finish_request(request, client_address)
        except Exception:
            self.handle_error(request, client_address)
        finally:
            self.shutdown_request(request)

    # override
    def process_request(self, request, client_address):
        self.threadpool.submit(
            self.process_request_thread, request, client_address)

    def server_close(self):
        super().server_close()
        self.threadpool.shutdown(True)

    def start(self):
        self.serve_forever()

    async def start_async(self):
        self.start()

    def _shutdown(self) -> None:
        _logger.info("shutdown http server in a seperate thread..")
        super().shutdown()

    def shutdown(self, force=False) -> None:
        threading.Thread(target=self._shutdown, daemon=False).start()

# file: http_servers/http_server.py
# -*- coding: utf-8 -*-
from ssl import PROTOCOL_TLS_SERVER, SSLContext
from typing import Dict,  Tuple
# from .coroutine_http_server import CoroutineHTTPServer
# from .threading_http_server import ThreadingHTTPServer
# from ..app_conf import _ControllerFunction, _WebsocketHandlerClass, AppConf, get_app_conf


class HttpServer:
    """Dispatcher Http server"""

    def map_filter(self, filter_conf):
        self.server.map_filter(filter_conf)

    def map_controller(self, ctrl: _ControllerFunction):
        self.server.map_controller(ctrl)

    def map_websocket_handler(self, handler: _WebsocketHandlerClass):
        self.server.map_websocket_handler(handler)

    def map_error_page(self, code, func):
        self.server.map_error_page(code, func)

    def __init__(self,
                 host: Tuple[str, int] = ('', 9090),
                 ssl: bool = False,
                 ssl_protocol: int = PROTOCOL_TLS_SERVER,
                 ssl_check_hostname: bool = False,
                 keyfile: str = "",
                 certfile: str = "",
                 keypass: str = "",
                 ssl_context: SSLContext = None,
                 resources: Dict[str, str] = {},
                 prefer_corountine=False,
                 max_workers: int = None,
                 connection_idle_time=None,
                 keep_alive=True,
                 keep_alive_max_request=None,
                 gzip_content_types=set(),
                 gzip_compress_level=9,
                 app_conf: AppConf = None):
        self.host = host
        self.__ready = False

        self.ssl = ssl

        if ssl:
            if ssl_context:
                self.ssl_ctx = ssl_context
            else:
                assert keyfile and certfile, "keyfile and certfile should be provided. "
                ssl_ctx = SSLContext(protocol=ssl_protocol)
                ssl_ctx.check_hostname = ssl_check_hostname
                ssl_ctx.load_cert_chain(
                    certfile=certfile, keyfile=keyfile, password=keypass)
                self.ssl_ctx = ssl_ctx
        else:
            self.ssl_ctx = None

        appconf = app_conf or get_app_conf()
        if prefer_corountine:
            _logger.info(
                f"Start server in corouting mode, listen to port: {self.host[1]}")
            self.server = CoroutineHTTPServer(
                self.host[0], self.host[1], self.ssl_ctx, resources, model_binding_conf=appconf.model_binding_conf)
        else:
            _logger.info(
                f"Start server in threading mixed mode, listen to port {self.host[1]}")
            self.server = ThreadingHTTPServer(
                self.host, resources, model_binding_conf=appconf.model_binding_conf, max_workers=max_workers)
            if self.ssl_ctx:
                self.server.socket = self.ssl_ctx.wrap_socket(
                    self.server.socket, server_side=True)

        self.server.gzip_compress_level = gzip_compress_level
        self.server.gzip_content_types = gzip_content_types

        filters = appconf._get_filters()
        # filter configuration
        for ft in filters:
            self.map_filter(ft)

        request_mappings = appconf._get_request_mappings()
        # request mapping
        for ctr in request_mappings:
            self.map_controller(ctr)

        ws_handlers = appconf._get_websocket_handlers()

        for wshandler in ws_handlers:
            self.map_websocket_handler(wshandler)

        err_pages = appconf._get_error_pages()
        for code, func in err_pages.items():
            self.map_error_page(code, func)
        self.server.keep_alive = keep_alive
        self.server.connection_idle_time = connection_idle_time
        self.server.keep_alive_max_request = keep_alive_max_request
        self.server.session_factory = appconf.session_factory

    @property
    def ready(self):
        return self.__ready

    def resources(self, res={}):
        self.server.res_conf = res

    def start(self):
        try:
            self.__ready = True
            elapsed_time = time.time() - start_time
            _logger.info(f"tweb engine ready after {elapsed_time:.3f} seconds.")
            self.server.start()
        except:
            self.__ready = False
            raise

    async def start_async(self):
        try:
            self.__ready = True
            elapsed_time = time.time() - start_time
            _logger.info(f"tweb engine ready after {elapsed_time:.3f} seconds.")
            await self.server.start_async()
        except:
            self.__ready = False
            raise

    def shutdown(self, force=False):
        # shutdown it in a seperate thread.
        self.server.shutdown(force)

# file: server.py
# -*- coding: utf-8 -*-
import os
import sys
import threading
import importlib
import re

from ssl import PROTOCOL_TLS_SERVER, SSLContext
from typing import Dict

# from .http_servers.http_server import HttpServer
# from .app_conf import AppConf
# from .utils.logger import get_logger
# _logger = get_logger("naja_atra.server")
__lock = threading.Lock()
_server: HttpServer = None


def _is_match(string="", regx=r""):
    if not regx:
        return True
    pattern = re.compile(regx)
    match = pattern.match(string)
    return True if match else False


def _to_module_name(fpath="", regx=r""):
    fname, fext = os.path.splitext(fpath)

    if fext != ".py":
        return
    mname = fname.replace(os.path.sep, '.')
    if _is_match(fpath, regx) or _is_match(fname, regx) or _is_match(mname, regx):
        return mname


def _load_all_modules(work_dir, pkg, regx):
    abs_folder = os.path.join(work_dir, pkg)
    if os.path.isfile(abs_folder):
        return [_to_module_name(pkg, regx)]
    if not os.path.exists(abs_folder):
        if abs_folder.endswith(".py"):
            _logger.warning(
                f"Cannot find package {pkg}, file [{abs_folder}] is not exist")
            return []
        if os.path.isfile(abs_folder + ".py"):
            return [_to_module_name(pkg + ".py", regx)]
        else:
            _logger.warning(
                f"Cannot find package {pkg}, file [{abs_folder}] is not exist")
            return []

    modules = []
    folders = []
    all_files = os.listdir(abs_folder)
    for f in all_files:
        if os.path.isfile(os.path.join(abs_folder, f)):
            mname = _to_module_name(os.path.join(pkg, f), regx)
            if mname:
                modules.append(mname)
        elif f != "__pycache__":
            folders.append(os.path.join(pkg, f))

    for folder in folders:
        modules += _load_all_modules(work_dir, folder, regx)
    return modules


def _import_module(mname):
    try:
        importlib.import_module(mname)
    except Exception as e:
        _logger.warning(f"Import moudle [{mname}] error! {e}")


def scan(base_dir: str = "", regx: str = r"", project_dir: str = "") -> None:
    """
    Scan the given directory to import controllers. 

    - base_dir: the directory to scan. 
    - regx: only include the modules that match this regular expression, if absent, all files will be included.
    - project_dir: the project directory, default to the entrance file directory. 

    """
    if project_dir:
        work_dir = project_dir
    else:
        mpath = os.path.dirname(sys.modules['__main__'].__file__)
        _logger.info(
            f"Project directory is not set, use the directory where the main module is in. {mpath}")
        work_dir = mpath
    modules = _load_all_modules(work_dir, base_dir, regx)

    for mname in modules:
        _logger.info(f"Import controllers from module: {mname}")
        _import_module(mname)


def _prepare_server(host: str = "",
                    port: int = 9090,
                    ssl: bool = False,
                    ssl_protocol: int = PROTOCOL_TLS_SERVER,
                    ssl_check_hostname: bool = False,
                    keyfile: str = "",
                    certfile: str = "",
                    keypass: str = "",
                    ssl_context: SSLContext = None,
                    resources: Dict[str, str] = {},
                    connection_idle_time=None,
                    keep_alive=True,
                    keep_alive_max_request=None,
                    gzip_content_types=set(),
                    gzip_compress_level=9,
                    prefer_coroutine=False,
                    app_conf: AppConf = None
                    ) -> None:
    with __lock:
        global _server
        if _server is not None:
            _server.shutdown()
        _server = HttpServer(host=(host, port),
                             ssl=ssl,
                             ssl_protocol=ssl_protocol,
                             ssl_check_hostname=ssl_check_hostname,
                             keyfile=keyfile,
                             certfile=certfile,
                             keypass=keypass,
                             ssl_context=ssl_context,
                             resources=resources,
                             connection_idle_time=connection_idle_time,
                             keep_alive=keep_alive,
                             keep_alive_max_request=keep_alive_max_request,
                             gzip_content_types=gzip_content_types,
                             gzip_compress_level=gzip_compress_level,
                             prefer_corountine=prefer_coroutine,
                             app_conf=app_conf)


def start(host: str = "",
          port: int = 9090,
          ssl: bool = False,
          ssl_protocol: int = PROTOCOL_TLS_SERVER,
          ssl_check_hostname: bool = False,
          keyfile: str = "",
          certfile: str = "",
          keypass: str = "",
          ssl_context: SSLContext = None,
          resources: Dict[str, str] = {},
          connection_idle_time=None,
          keep_alive=True,
          keep_alive_max_request=None,
          gzip_content_types=set(),
          gzip_compress_level=9,
          prefer_coroutine=False,
          app_conf: AppConf = None, **kwargs) -> None:
    _prepare_server(
        host=host,
        port=port,
        ssl=ssl,
        ssl_protocol=ssl_protocol,
        ssl_check_hostname=ssl_check_hostname,
        keyfile=keyfile,
        certfile=certfile,
        keypass=keypass,
        ssl_context=ssl_context,
        resources=resources,
        connection_idle_time=connection_idle_time,
        keep_alive=keep_alive,
        keep_alive_max_request=keep_alive_max_request,
        gzip_content_types=gzip_content_types,
        gzip_compress_level=gzip_compress_level,
        prefer_coroutine=prefer_coroutine,
        app_conf=app_conf
    )
    # start the server
    _server.start()


async def start_async(host: str = "",
                      port: int = 9090,
                      ssl: bool = False,
                      ssl_protocol: int = PROTOCOL_TLS_SERVER,
                      ssl_check_hostname: bool = False,
                      keyfile: str = "",
                      certfile: str = "",
                      keypass: str = "",
                      ssl_context: SSLContext = None,
                      resources: Dict[str, str] = {},
                      connection_idle_time=None,
                      keep_alive=True,
                      keep_alive_max_request=None,
                      gzip_content_types=set(),
                      gzip_compress_level=9,
                      prefer_coroutine=True,
                      app_conf: AppConf = None) -> None:
    _prepare_server(
        host=host,
        port=port,
        ssl=ssl,
        ssl_protocol=ssl_protocol,
        ssl_check_hostname=ssl_check_hostname,
        keyfile=keyfile,
        certfile=certfile,
        keypass=keypass,
        ssl_context=ssl_context,
        resources=resources,
        connection_idle_time=connection_idle_time,
        keep_alive=keep_alive,
        keep_alive_max_request=keep_alive_max_request,
        gzip_content_types=gzip_content_types,
        gzip_compress_level=gzip_compress_level,
        prefer_coroutine=prefer_coroutine,
        app_conf=app_conf
    )

    # start the server
    await _server.start_async()


def is_ready() -> bool:
    return _server and _server.ready


def stop(force=False) -> None:
    with __lock:
        global _server
        if _server is not None:
            _logger.info("Shutting down server...")
            _server.shutdown(force)
            _server = None
        else:
            _logger.warning("Server is not ready yet.")
    print('Server stopped.')

# file: default main()
import os
import sys
import signal
import getopt

def on_sig_term(signum, frame):
    _logger.info(f"Receive signal [{signum}], stop server now...")
    stop()

def main(argv):
    def print_help():
        print("""
        python3 -m tweb [options]

        Options:
        -h    --help        Show this message and exit.
        -p    --port        Specify alternate port (default: 9090)
        -b    --bind        Specify alternate bind address (default: all interfaces)
        -s    --scan        Scan this path to find controllers, if absent, will scan the current work directory
            --regex       Only import the files that macthes this regular expresseion.
        -r    --resources   Specify the resource directory
            --loglevel    Specify log level (default: info)
        """)
    try:
        opts = getopt.getopt(argv, "p:b:s:r:h",
                             ["port=", "bind=", "scan=", "resources=", "regex=", "loglevel=", "help"])[0]
        opts = dict(opts)
        if "-h" in opts or "--help" in opts:
            print_help()
            return
        port = int(opts.get("-p", opts.get("--port", "9090")))
        scan_path = opts.get("-s", opts.get("--scan", "./"))
        regex = opts.get("--regex", ".*controller.py")
        res_dir = opts.get("-r", opts.get("--resources", os.getcwd()))
        binding_host = opts.get("-b", opts.get("--bind", "0.0.0.0"))
        log_level = opts.get("--loglevel", "INFO")

        if log_level:
            set_level(log_level)
        signal.signal(signal.SIGTERM, on_sig_term)
        signal.signal(signal.SIGINT, on_sig_term)
        _logger.info(f'web static dir: [{res_dir}]')
        scan(regx=regex, project_dir=scan_path)
        start(host=binding_host,
              port=port,
              resources={"/**": res_dir},
              keep_alive=False,
              prefer_coroutine=True)
    except Exception as e:
        print(f"Start server error: {e}")
        print_help()


if __name__ == "__main__":
    main(sys.argv[1:])
